From 54eb745c8e9190758d205dafb8ed9bfdb8658552 Mon Sep 17 00:00:00 2001 From: huntharo Date: Wed, 11 Mar 2026 20:51:00 -0400 Subject: [PATCH 01/34] feat: add vectorlite cluster experiment --- apps/cli/src/main.ts | 18 +++ packages/api-core/package.json | 7 +- packages/api-core/src/service.ts | 186 ++++++++++++++++++++++++++++++- 3 files changed, 207 insertions(+), 4 deletions(-) diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts index 7919338..6cc9087 100644 --- a/apps/cli/src/main.ts +++ b/apps/cli/src/main.ts @@ -24,6 +24,7 @@ type CommandName = | 'purge-comments' | 'embed' | 'cluster' + | 'cluster-experiment' | 'clusters' | 'cluster-detail' | 'search' @@ -430,6 +431,8 @@ export function parseRepoFlags(command: CommandName, args: string[]): ParsedRepo query: { type: 'string' }, mode: { type: 'string' }, k: { type: 'string' }, + backend: { type: 'string' }, + 'candidate-k': { type: 'string' }, threshold: { type: 'string' }, port: { type: 'string' }, id: { type: 'string' }, @@ -934,6 +937,21 @@ export async function run( heapDiagnostics?.dispose(); } } + case 'cluster-experiment': { + const { owner, repo, values } = parseRepoFlags(rest); + const backend = values.backend === 'vectorlite' ? values.backend : undefined; + const result = getService().clusterExperiment({ + owner, + repo, + backend, + k: typeof values.k === 'string' ? Number(values.k) : undefined, + minScore: typeof values.threshold === 'string' ? Number(values.threshold) : undefined, + candidateK: typeof values['candidate-k'] === 'string' ? Number(values['candidate-k']) : undefined, + onProgress: writeProgress, + }); + stdout.write(`${JSON.stringify(result, null, 2)}\n`); + return; + } case 'clusters': { const { owner, repo, values } = parseRepoFlags('clusters', rest); const sort = parseEnum('clusters', 'sort', values.sort, ['recent', 'size']); diff --git a/packages/api-core/package.json b/packages/api-core/package.json index f644160..6db0773 100644 --- a/packages/api-core/package.json +++ b/packages/api-core/package.json @@ -46,13 +46,14 @@ }, "dependencies": { "@ghcrawl/api-contract": "workspace:*", - "@shutterstock/p-map-iterable": "^1.1.2", "@octokit/plugin-retry": "^8.0.3", "@octokit/plugin-throttling": "^11.0.1", - "octokit": "^5.0.3", - "better-sqlite3": "^12.8.0", + "@shutterstock/p-map-iterable": "^1.1.2", + "better-sqlite3": "^12.2.0", "dotenv": "^17.2.2", + "octokit": "^5.0.3", "openai": "^6.33.0", + "vectorlite": "^0.2.0", "zod": "^4.3.6" } } diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index 21f05f8..7480088 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -1,7 +1,10 @@ import http from 'node:http'; import crypto from 'node:crypto'; +import fs from 'node:fs'; import { existsSync } from 'node:fs'; +import { createRequire } from 'node:module'; import os from 'node:os'; +import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { Worker } from 'node:worker_threads'; @@ -62,7 +65,7 @@ import { openDb, type SqliteDatabase } from './db/sqlite.js'; import { buildCanonicalDocument, isBotLikeAuthor } from './documents/normalize.js'; import { makeGitHubClient, type GitHubClient } from './github/client.js'; import { OpenAiProvider, type AiProvider } from './openai/provider.js'; -import { cosineSimilarity, normalizeEmbedding, rankNearestNeighbors } from './search/exact.js'; +import { cosineSimilarity, dotProduct, normalizeEmbedding, rankNearestNeighbors, rankNearestNeighborsByScore } from './search/exact.js'; type RunTable = 'sync_runs' | 'summary_runs' | 'embedding_runs' | 'cluster_runs'; @@ -114,6 +117,25 @@ type StoredEmbeddingRow = ThreadRow & { embedding_json: string; }; +type ParsedStoredEmbeddingRow = Omit & { + embedding: number[]; + normalizedEmbedding: number[]; + embeddingNorm: number; +}; + +type ClusterExperimentResult = { + backend: 'vectorlite'; + repository: RepositoryDto; + tempDbPath: string; + threads: number; + sourceKinds: number; + edges: number; + clusters: number; + durationMs: number; + indexBuildMs: number; + queryMs: number; + candidateK: number; +}; type EmbeddingWorkset = { rows: Array<{ id: number; @@ -261,6 +283,7 @@ const CLUSTER_PARALLEL_MIN_EMBEDDINGS = 5000; const EMBED_ESTIMATED_CHARS_PER_TOKEN = 3; const EMBED_MAX_ITEM_TOKENS = 7000; const EMBED_MAX_BATCH_TOKENS = 250000; +const requireFromHere = createRequire(import.meta.url); const EMBED_TRUNCATION_MARKER = '\n\n[truncated for embedding]'; const EMBED_CONTEXT_RETRY_ATTEMPTS = 5; const EMBED_CONTEXT_RETRY_FALLBACK_SHRINK_RATIO = 0.9; @@ -1121,6 +1144,163 @@ export class GHCrawlService { } } + clusterExperiment(params: { + owner: string; + repo: string; + backend?: 'vectorlite'; + minScore?: number; + k?: number; + candidateK?: number; + onProgress?: (message: string) => void; + }): ClusterExperimentResult { + const backend = params.backend ?? 'vectorlite'; + if (backend !== 'vectorlite') { + throw new Error(`Unsupported experimental cluster backend: ${backend}`); + } + + const repository = this.requireRepository(params.owner, params.repo); + const { items, sourceKinds } = this.loadClusterableThreadMeta(repository.id); + const minScore = params.minScore ?? 0.82; + const k = params.k ?? 6; + const candidateK = Math.max(k, params.candidateK ?? Math.max(k * 16, 64)); + + params.onProgress?.( + `[cluster-experiment] loaded ${items.length} embedded thread(s) across ${sourceKinds.length} source kind(s) for ${repository.fullName} backend=${backend} k=${k} candidateK=${candidateK} minScore=${minScore}`, + ); + + const rows = this.loadParsedStoredEmbeddings(repository.id); + const bySource = new Map(); + for (const row of rows) { + const list = bySource.get(row.source_kind) ?? []; + list.push(row); + bySource.set(row.source_kind, list); + } + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghcrawl-vectorlite-')); + const tempDbPath = path.join(tempDir, 'cluster-experiment.db'); + const tempDb = openDb(tempDbPath); + let indexBuildMs = 0; + let queryMs = 0; + + try { + tempDb.pragma('journal_mode = MEMORY'); + tempDb.pragma('synchronous = OFF'); + tempDb.pragma('temp_store = MEMORY'); + const vectorlite = requireFromHere('vectorlite') as { vectorlitePath: () => string }; + (tempDb as SqliteDatabase & { loadExtension: (extensionPath: string) => void }).loadExtension(vectorlite.vectorlitePath()); + + const experimentStartedAt = Date.now(); + const aggregated = new Map }>(); + + for (const sourceKind of sourceKinds) { + const sourceRows = bySource.get(sourceKind) ?? []; + if (sourceRows.length === 0) { + continue; + } + + const tableName = `vector_${sourceKind}`; + const dimension = sourceRows[0]?.normalizedEmbedding.length ?? 0; + const safeCandidateK = Math.min(candidateK, Math.max(1, sourceRows.length - 1)); + + params.onProgress?.( + `[cluster-experiment] building ${sourceKind} HNSW index with ${sourceRows.length} vector(s)`, + ); + const indexStartedAt = Date.now(); + tempDb.exec( + `create virtual table ${tableName} using vectorlite(vec float32[${dimension}], hnsw(max_elements=${sourceRows.length}));`, + ); + const insert = tempDb.prepare(`insert into ${tableName}(rowid, vec) values (?, ?)`); + tempDb.transaction(() => { + for (const row of sourceRows) { + insert.run(row.id, this.normalizedEmbeddingBuffer(row.normalizedEmbedding)); + } + })(); + indexBuildMs += Date.now() - indexStartedAt; + + const rowsById = new Map(sourceRows.map((row) => [row.id, row])); + const queryStartedAt = Date.now(); + const query = tempDb.prepare( + `select rowid from ${tableName} where knn_search(vec, knn_param(?, ${safeCandidateK + 1}))`, + ); + let processed = 0; + let lastProgressAt = Date.now(); + for (const row of sourceRows) { + const candidates = query.all(this.normalizedEmbeddingBuffer(row.normalizedEmbedding)) as Array<{ rowid: number }>; + const ranked = rankNearestNeighborsByScore(candidates, { + limit: k, + minScore, + score: (candidate) => { + if (candidate.rowid === row.id) { + return -1; + } + const candidateRow = rowsById.get(candidate.rowid); + if (!candidateRow) { + return -1; + } + return dotProduct(row.normalizedEmbedding, candidateRow.normalizedEmbedding); + }, + }); + let addedThisRow = 0; + for (const candidate of ranked) { + const candidateRow = rowsById.get(candidate.item.rowid); + if (!candidateRow) continue; + const score = candidate.score; + const key = this.edgeKey(row.id, candidateRow.id); + const existing = aggregated.get(key); + if (existing) { + existing.score = Math.max(existing.score, score); + existing.sourceKinds.add(sourceKind); + continue; + } + aggregated.set(key, { + leftThreadId: Math.min(row.id, candidateRow.id), + rightThreadId: Math.max(row.id, candidateRow.id), + score, + sourceKinds: new Set([sourceKind]), + }); + addedThisRow += 1; + } + processed += 1; + const now = Date.now(); + if (params.onProgress && now - lastProgressAt >= CLUSTER_PROGRESS_INTERVAL_MS) { + params.onProgress( + `[cluster-experiment] querying ${sourceKind} index ${processed}/${sourceRows.length} current_edges=${aggregated.size} added_this_step=${addedThisRow}`, + ); + lastProgressAt = now; + } + } + queryMs += Date.now() - queryStartedAt; + tempDb.exec(`drop table ${tableName}`); + } + + const clusters = buildClusters( + items.map((item) => ({ threadId: item.id, number: item.number, title: item.title })), + Array.from(aggregated.values()).map((entry) => ({ + leftThreadId: entry.leftThreadId, + rightThreadId: entry.rightThreadId, + score: entry.score, + })), + ); + + return { + backend, + repository, + tempDbPath, + threads: items.length, + sourceKinds: sourceKinds.length, + edges: aggregated.size, + clusters: clusters.length, + durationMs: Date.now() - experimentStartedAt, + indexBuildMs, + queryMs, + candidateK, + }; + } finally { + tempDb.close(); + fs.rmSync(tempDir, { recursive: true, force: true }); + } + } + async searchRepository(params: { owner: string; repo: string; @@ -2871,6 +3051,10 @@ export class GHCrawlService { })); } + private normalizedEmbeddingBuffer(values: number[]): Buffer { + return Buffer.from(Float32Array.from(values).buffer); + } + private loadClusterableThreadMeta(repoId: number): { items: Array<{ id: number; number: number; title: string }>; sourceKinds: EmbeddingSourceKind[]; From 621166c860436de855006e0a2236974ece1df775 Mon Sep 17 00:00:00 2001 From: huntharo Date: Thu, 12 Mar 2026 10:37:46 -0400 Subject: [PATCH 02/34] ci: compare cluster perf backends in pull requests --- .github/workflows/ci.yml | 146 +++++++++++++++++- .../api-core/src/cluster/perf.integration.ts | 64 ++++++-- 2 files changed, 193 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf87a71..1abc7e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,17 +70,157 @@ jobs: - name: Install Node.js and dependencies uses: ./.github/actions/configure-nodejs - - name: Run cluster performance integration test + - name: Create baseline worktree for pull requests + if: ${{ github.event_name == 'pull_request' }} + run: git worktree add --detach "$RUNNER_TEMP/cluster-perf-base" "${{ github.event.pull_request.base.sha }}" + + - name: Install dependencies for baseline worktree + if: ${{ github.event_name == 'pull_request' }} + working-directory: ${{ runner.temp }}/cluster-perf-base + run: pnpm install --frozen-lockfile + + - name: Run baseline exact cluster perf + if: ${{ github.event_name == 'pull_request' }} + env: + GHCRAWL_CLUSTER_PERF_IGNORE_THRESHOLD: 1 + GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf-base-exact.json + working-directory: ${{ runner.temp }}/cluster-perf-base + run: pnpm test:cluster-perf + + - name: Run PR head exact cluster perf env: - GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf.json + GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf-head-exact.json run: pnpm test:cluster-perf + - name: Run PR head vectorlite cluster perf + if: ${{ github.event_name == 'pull_request' }} + env: + GHCRAWL_CLUSTER_PERF_BACKEND: vectorlite + GHCRAWL_CLUSTER_PERF_IGNORE_THRESHOLD: 1 + GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf-head-vectorlite.json + run: pnpm test:cluster-perf + + - name: Build combined cluster perf report + env: + GH_EVENT_NAME: ${{ github.event_name }} + BASE_REPORT_PATH: ${{ runner.temp }}/cluster-perf-base-exact.json + HEAD_EXACT_REPORT_PATH: ${{ runner.temp }}/cluster-perf-head-exact.json + HEAD_VECTORLITE_REPORT_PATH: ${{ runner.temp }}/cluster-perf-head-vectorlite.json + COMBINED_REPORT_PATH: ${{ runner.temp }}/cluster-perf.json + run: | + node <<'EOF' + const fs = require('node:fs'); + + function readReport(path) { + if (!path || !fs.existsSync(path)) { + return null; + } + return JSON.parse(fs.readFileSync(path, 'utf8')); + } + + function formatDurationMs(durationMs) { + if (!Number.isFinite(durationMs)) return 'n/a'; + if (durationMs < 1000) return `${durationMs.toFixed(1)} ms`; + const totalSeconds = durationMs / 1000; + if (totalSeconds < 60) return `${totalSeconds.toFixed(2)} s`; + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds - minutes * 60; + return `${minutes}m ${seconds.toFixed(1)}s`; + } + + function formatPercent(value) { + const sign = value > 0 ? '+' : ''; + return `${sign}${value.toFixed(1)}%`; + } + + const eventName = process.env.GH_EVENT_NAME; + const base = readReport(process.env.BASE_REPORT_PATH); + const headExact = readReport(process.env.HEAD_EXACT_REPORT_PATH); + const headVectorlite = readReport(process.env.HEAD_VECTORLITE_REPORT_PATH); + + if (!headExact) { + throw new Error('Missing PR/head exact cluster perf report'); + } + + const lines = [ + '## Cluster Performance', + '', + `- Head exact fixture median: ${formatDurationMs(headExact.result.medianMs)}`, + `- Head exact projected openclaw/openclaw: ${formatDurationMs(headExact.result.projectedOpenclawMs)}`, + ]; + + if (eventName === 'pull_request' && base) { + const exactDeltaMs = headExact.result.medianMs - base.result.medianMs; + const exactDeltaPercent = base.result.medianMs > 0 ? (exactDeltaMs / base.result.medianMs) * 100 : 0; + const exactProjectedDeltaMs = headExact.result.projectedOpenclawMs - base.result.projectedOpenclawMs; + const exactProjectedDeltaPercent = + base.result.projectedOpenclawMs > 0 ? (exactProjectedDeltaMs / base.result.projectedOpenclawMs) * 100 : 0; + lines.push( + `- Base exact fixture median: ${formatDurationMs(base.result.medianMs)}`, + `- Exact delta vs base: ${formatDurationMs(exactDeltaMs)} (${formatPercent(exactDeltaPercent)})`, + `- Base exact projected openclaw/openclaw: ${formatDurationMs(base.result.projectedOpenclawMs)}`, + `- Exact projected delta vs base: ${formatDurationMs(exactProjectedDeltaMs)} (${formatPercent(exactProjectedDeltaPercent)})`, + ); + } + + if (headVectorlite) { + const vectorliteDeltaMs = headVectorlite.result.medianMs - headExact.result.medianMs; + const vectorliteDeltaPercent = + headExact.result.medianMs > 0 ? (vectorliteDeltaMs / headExact.result.medianMs) * 100 : 0; + const vectorliteProjectedDeltaMs = headVectorlite.result.projectedOpenclawMs - headExact.result.projectedOpenclawMs; + const vectorliteProjectedDeltaPercent = + headExact.result.projectedOpenclawMs > 0 ? (vectorliteProjectedDeltaMs / headExact.result.projectedOpenclawMs) * 100 : 0; + lines.push( + '', + `- Head vectorlite fixture median: ${formatDurationMs(headVectorlite.result.medianMs)}`, + `- Vectorlite delta vs head exact: ${formatDurationMs(vectorliteDeltaMs)} (${formatPercent(vectorliteDeltaPercent)})`, + `- Head vectorlite projected openclaw/openclaw: ${formatDurationMs(headVectorlite.result.projectedOpenclawMs)}`, + `- Vectorlite projected delta vs head exact: ${formatDurationMs(vectorliteProjectedDeltaMs)} (${formatPercent(vectorliteProjectedDeltaPercent)})`, + ); + } + + lines.push('', '### Raw Runs', '', '#### Head Exact', '', headExact.summary); + if (eventName === 'pull_request' && base) { + lines.push('', '#### Base Exact', '', base.summary); + } + if (headVectorlite) { + lines.push('', '#### Head Vectorlite', '', headVectorlite.summary); + } + + const summary = `${lines.join('\n')}\n`; + fs.writeFileSync( + process.env.COMBINED_REPORT_PATH, + JSON.stringify( + { + status: 'PASS', + summary, + reports: { + baseExact: base, + headExact, + headVectorlite, + }, + }, + null, + 2, + ) + '\n', + ); + + const summaryPath = process.env.GITHUB_STEP_SUMMARY; + if (summaryPath) { + fs.appendFileSync(summaryPath, summary); + } + EOF + - name: Upload cluster performance report artifact if: ${{ always() }} uses: actions/upload-artifact@v7 with: name: cluster-perf-report - path: ${{ runner.temp }}/cluster-perf.json + path: | + ${{ runner.temp }}/cluster-perf.json + ${{ runner.temp }}/cluster-perf-base-exact.json + ${{ runner.temp }}/cluster-perf-head-exact.json + ${{ runner.temp }}/cluster-perf-head-vectorlite.json if-no-files-found: ignore cli-smoke: diff --git a/packages/api-core/src/cluster/perf.integration.ts b/packages/api-core/src/cluster/perf.integration.ts index b8bcf5d..d4cc1b6 100644 --- a/packages/api-core/src/cluster/perf.integration.ts +++ b/packages/api-core/src/cluster/perf.integration.ts @@ -37,6 +37,7 @@ type PerfBaseline = { }; type PerfRunResult = { + backend: 'exact' | 'vectorlite'; sampleDurationsMs: number[]; medianMs: number; baselineMedianMs: number; @@ -68,6 +69,27 @@ function shouldBootstrapBaseline(): boolean { return process.env.GHCRAWL_CLUSTER_PERF_BOOTSTRAP === '1'; } +function shouldIgnoreRegressionThreshold(): boolean { + return process.env.GHCRAWL_CLUSTER_PERF_IGNORE_THRESHOLD === '1'; +} + +function getPerfBackend(): 'exact' | 'vectorlite' { + return process.env.GHCRAWL_CLUSTER_PERF_BACKEND === 'vectorlite' ? 'vectorlite' : 'exact'; +} + +function assertBenchmarkShape( + result: { clusters: number; edges: number }, + baseline: PerfBaseline, + backend: 'exact' | 'vectorlite', +): void { + if (backend === 'exact') { + assert.equal(result.clusters, baseline.fixture.clusterCount); + } else { + assert.ok(result.clusters > 0); + } + assert.ok(result.edges > baseline.fixture.clusterCount); +} + function formatDurationMs(durationMs: number): string { if (!Number.isFinite(durationMs)) return 'n/a'; if (durationMs < 1000) { @@ -279,16 +301,29 @@ function seedBenchmarkDatabase(dbPath: string, baseline: PerfBaseline): void { } } -async function runSingleCluster(dbPath: string, baseline: PerfBaseline): Promise<{ durationMs: number; clusters: number; edges: number }> { +async function runSingleCluster( + dbPath: string, + baseline: PerfBaseline, + backend: 'exact' | 'vectorlite', +): Promise<{ durationMs: number; clusters: number; edges: number }> { const service = createService(dbPath); try { const startedAt = performance.now(); - const result = await service.clusterRepository({ - owner: 'openclaw', - repo: 'openclaw', - k: baseline.fixture.k, - minScore: baseline.fixture.minScore, - }); + const result = + backend === 'vectorlite' + ? service.clusterExperiment({ + owner: 'openclaw', + repo: 'openclaw', + backend: 'vectorlite', + k: baseline.fixture.k, + minScore: baseline.fixture.minScore, + }) + : await service.clusterRepository({ + owner: 'openclaw', + repo: 'openclaw', + k: baseline.fixture.k, + minScore: baseline.fixture.minScore, + }); const durationMs = performance.now() - startedAt; return { durationMs, clusters: result.clusters, edges: result.edges }; } finally { @@ -297,6 +332,7 @@ async function runSingleCluster(dbPath: string, baseline: PerfBaseline): Promise } async function measureBenchmark(baseline: PerfBaseline): Promise { + const backend = getPerfBackend(); if (baseline.baseline.fixtureMedianMs <= 0 && !shouldBootstrapBaseline()) { throw new Error( `Cluster perf baseline is not set in ${BASELINE_PATH}. Run the benchmark once, then record fixtureMedianMs before enforcing regressions.`, @@ -317,9 +353,8 @@ async function measureBenchmark(baseline: PerfBaseline): Promise for (let warmupIndex = 0; warmupIndex < warmupRuns; warmupIndex += 1) { const warmupDbPath = path.join(tempRoot, `warmup-${warmupIndex}.sqlite`); fs.copyFileSync(seedDbPath, warmupDbPath); - const warmupResult = await runSingleCluster(warmupDbPath, baseline); - assert.equal(warmupResult.clusters, baseline.fixture.clusterCount); - assert.ok(warmupResult.edges > baseline.fixture.clusterCount); + const warmupResult = await runSingleCluster(warmupDbPath, baseline, backend); + assertBenchmarkShape(warmupResult, baseline, backend); } while (sampleDurationsMs.length < baseline.benchmark.maxSamples) { @@ -328,9 +363,8 @@ async function measureBenchmark(baseline: PerfBaseline): Promise const runDbPath = path.join(tempRoot, `run-${runCounter}.sqlite`); runCounter += 1; fs.copyFileSync(seedDbPath, runDbPath); - const result = await runSingleCluster(runDbPath, baseline); - assert.equal(result.clusters, baseline.fixture.clusterCount); - assert.ok(result.edges > baseline.fixture.clusterCount); + const result = await runSingleCluster(runDbPath, baseline, backend); + assertBenchmarkShape(result, baseline, backend); } sampleDurationsMs.push(performance.now() - sampleStartedAt); @@ -350,6 +384,7 @@ async function measureBenchmark(baseline: PerfBaseline): Promise const projectedDeltaPercent = (projectedDeltaMs / projectedBaselineOpenclawMs) * 100; return { + backend, sampleDurationsMs, medianMs, baselineMedianMs, @@ -384,6 +419,7 @@ function buildSummary(result: PerfRunResult): string { return [ '## Cluster Performance', '', + `- Backend: ${result.backend}`, `- Status: ${status}`, `- Fixture median: ${formatDurationMs(result.medianMs)} (${result.samples} samples, ${result.runsPerSample} cluster rebuilds/sample)`, `- Fixture baseline: ${formatDurationMs(result.baselineMedianMs)}`, @@ -430,7 +466,7 @@ async function main(): Promise { const result = await measureBenchmark(baseline); const summary = buildSummary(result); const bootstrap = shouldBootstrapBaseline(); - const shouldFail = !bootstrap && result.deltaPercent > result.maxRegressionPercent; + const shouldFail = !bootstrap && !shouldIgnoreRegressionThreshold() && result.deltaPercent > result.maxRegressionPercent; process.stdout.write(`${summary}\n`); const suggestedBaseline = buildSuggestedBaseline(result); From 780762da790102b5b499bf335eb94bedce9acc7c Mon Sep 17 00:00:00 2001 From: huntharo Date: Thu, 12 Mar 2026 11:07:35 -0400 Subject: [PATCH 03/34] ci: report post-index vectorlite perf timings --- .github/workflows/ci.yml | 6 +++--- packages/api-core/src/cluster/perf.integration.ts | 12 ++++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1abc7e2..f9d4d02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,7 +145,7 @@ jobs: const lines = [ '## Cluster Performance', '', - `- Head exact fixture median: ${formatDurationMs(headExact.result.medianMs)}`, + `- Head exact fixture median (full-run): ${formatDurationMs(headExact.result.medianMs)}`, `- Head exact projected openclaw/openclaw: ${formatDurationMs(headExact.result.projectedOpenclawMs)}`, ]; @@ -156,7 +156,7 @@ jobs: const exactProjectedDeltaPercent = base.result.projectedOpenclawMs > 0 ? (exactProjectedDeltaMs / base.result.projectedOpenclawMs) * 100 : 0; lines.push( - `- Base exact fixture median: ${formatDurationMs(base.result.medianMs)}`, + `- Base exact fixture median (full-run): ${formatDurationMs(base.result.medianMs)}`, `- Exact delta vs base: ${formatDurationMs(exactDeltaMs)} (${formatPercent(exactDeltaPercent)})`, `- Base exact projected openclaw/openclaw: ${formatDurationMs(base.result.projectedOpenclawMs)}`, `- Exact projected delta vs base: ${formatDurationMs(exactProjectedDeltaMs)} (${formatPercent(exactProjectedDeltaPercent)})`, @@ -172,7 +172,7 @@ jobs: headExact.result.projectedOpenclawMs > 0 ? (vectorliteProjectedDeltaMs / headExact.result.projectedOpenclawMs) * 100 : 0; lines.push( '', - `- Head vectorlite fixture median: ${formatDurationMs(headVectorlite.result.medianMs)}`, + `- Head vectorlite fixture median (post-index): ${formatDurationMs(headVectorlite.result.medianMs)}`, `- Vectorlite delta vs head exact: ${formatDurationMs(vectorliteDeltaMs)} (${formatPercent(vectorliteDeltaPercent)})`, `- Head vectorlite projected openclaw/openclaw: ${formatDurationMs(headVectorlite.result.projectedOpenclawMs)}`, `- Vectorlite projected delta vs head exact: ${formatDurationMs(vectorliteProjectedDeltaMs)} (${formatPercent(vectorliteProjectedDeltaPercent)})`, diff --git a/packages/api-core/src/cluster/perf.integration.ts b/packages/api-core/src/cluster/perf.integration.ts index d4cc1b6..f32fa24 100644 --- a/packages/api-core/src/cluster/perf.integration.ts +++ b/packages/api-core/src/cluster/perf.integration.ts @@ -38,6 +38,7 @@ type PerfBaseline = { type PerfRunResult = { backend: 'exact' | 'vectorlite'; + timingBasis: 'full-run' | 'post-index'; sampleDurationsMs: number[]; medianMs: number; baselineMedianMs: number; @@ -324,7 +325,11 @@ async function runSingleCluster( k: baseline.fixture.k, minScore: baseline.fixture.minScore, }); - const durationMs = performance.now() - startedAt; + const wallClockDurationMs = performance.now() - startedAt; + const durationMs = + backend === 'vectorlite' + ? Math.max(0, result.durationMs - result.indexBuildMs) + : wallClockDurationMs; return { durationMs, clusters: result.clusters, edges: result.edges }; } finally { service.close(); @@ -385,6 +390,7 @@ async function measureBenchmark(baseline: PerfBaseline): Promise return { backend, + timingBasis: backend === 'vectorlite' ? 'post-index' : 'full-run', sampleDurationsMs, medianMs, baselineMedianMs, @@ -409,6 +415,7 @@ function buildSummary(result: PerfRunResult): string { const status = result.deltaPercent > result.maxRegressionPercent ? 'FAIL' : 'PASS'; const sampleList = result.sampleDurationsMs.map((value) => formatDurationMs(value)).join(', '); const suggestedBaseline = buildSuggestedBaseline(result); + const timingLabel = result.timingBasis === 'post-index' ? 'Fixture median (post-index)' : 'Fixture median'; const bootstrapLine = result.baselineMedianMs === result.medianMs ? '- Bootstrap mode: using the current fixture median as the provisional baseline' @@ -420,8 +427,9 @@ function buildSummary(result: PerfRunResult): string { '## Cluster Performance', '', `- Backend: ${result.backend}`, + `- Timing basis: ${result.timingBasis}`, `- Status: ${status}`, - `- Fixture median: ${formatDurationMs(result.medianMs)} (${result.samples} samples, ${result.runsPerSample} cluster rebuilds/sample)`, + `- ${timingLabel}: ${formatDurationMs(result.medianMs)} (${result.samples} samples, ${result.runsPerSample} cluster rebuilds/sample)`, `- Fixture baseline: ${formatDurationMs(result.baselineMedianMs)}`, `- Fixture delta: ${formatDurationMs(result.deltaMs)} (${formatPercent(result.deltaPercent)})`, `- Projected openclaw/openclaw duration: ${formatDurationMs(result.projectedOpenclawMs)}`, From 9968cbe76d32c2843092b0951495bbcecd286e3e Mon Sep 17 00:00:00 2001 From: huntharo Date: Thu, 12 Mar 2026 14:30:56 -0400 Subject: [PATCH 04/34] fix: repair cluster perf ci checks --- .github/workflows/ci.yml | 2 + .../api-core/src/cluster/perf.integration.ts | 39 +++++++++---------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9d4d02..877f870 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,6 +66,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Install Node.js and dependencies uses: ./.github/actions/configure-nodejs diff --git a/packages/api-core/src/cluster/perf.integration.ts b/packages/api-core/src/cluster/perf.integration.ts index f32fa24..7f45ea1 100644 --- a/packages/api-core/src/cluster/perf.integration.ts +++ b/packages/api-core/src/cluster/perf.integration.ts @@ -310,26 +310,25 @@ async function runSingleCluster( const service = createService(dbPath); try { const startedAt = performance.now(); - const result = - backend === 'vectorlite' - ? service.clusterExperiment({ - owner: 'openclaw', - repo: 'openclaw', - backend: 'vectorlite', - k: baseline.fixture.k, - minScore: baseline.fixture.minScore, - }) - : await service.clusterRepository({ - owner: 'openclaw', - repo: 'openclaw', - k: baseline.fixture.k, - minScore: baseline.fixture.minScore, - }); - const wallClockDurationMs = performance.now() - startedAt; - const durationMs = - backend === 'vectorlite' - ? Math.max(0, result.durationMs - result.indexBuildMs) - : wallClockDurationMs; + if (backend === 'vectorlite') { + const result = service.clusterExperiment({ + owner: 'openclaw', + repo: 'openclaw', + backend: 'vectorlite', + k: baseline.fixture.k, + minScore: baseline.fixture.minScore, + }); + const durationMs = Math.max(0, result.durationMs - result.indexBuildMs); + return { durationMs, clusters: result.clusters, edges: result.edges }; + } + + const result = await service.clusterRepository({ + owner: 'openclaw', + repo: 'openclaw', + k: baseline.fixture.k, + minScore: baseline.fixture.minScore, + }); + const durationMs = performance.now() - startedAt; return { durationMs, clusters: result.clusters, edges: result.edges }; } finally { service.close(); From dc40c8dfc5e3b7b2726c796de4dfe0a8d0354bda Mon Sep 17 00:00:00 2001 From: huntharo Date: Thu, 12 Mar 2026 14:45:03 -0400 Subject: [PATCH 05/34] ci: warm vectorlite perf before reporting --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 877f870..13fc658 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,7 +100,11 @@ jobs: GHCRAWL_CLUSTER_PERF_BACKEND: vectorlite GHCRAWL_CLUSTER_PERF_IGNORE_THRESHOLD: 1 GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf-head-vectorlite.json - run: pnpm test:cluster-perf + run: | + for attempt in 1 2 3; do + echo "Vectorlite perf attempt ${attempt}/3" + pnpm test:cluster-perf + done - name: Build combined cluster perf report env: @@ -174,7 +178,7 @@ jobs: headExact.result.projectedOpenclawMs > 0 ? (vectorliteProjectedDeltaMs / headExact.result.projectedOpenclawMs) * 100 : 0; lines.push( '', - `- Head vectorlite fixture median (post-index): ${formatDurationMs(headVectorlite.result.medianMs)}`, + `- Head vectorlite fixture median (post-index, run 3/3): ${formatDurationMs(headVectorlite.result.medianMs)}`, `- Vectorlite delta vs head exact: ${formatDurationMs(vectorliteDeltaMs)} (${formatPercent(vectorliteDeltaPercent)})`, `- Head vectorlite projected openclaw/openclaw: ${formatDurationMs(headVectorlite.result.projectedOpenclawMs)}`, `- Vectorlite projected delta vs head exact: ${formatDurationMs(vectorliteProjectedDeltaMs)} (${formatPercent(vectorliteProjectedDeltaPercent)})`, From 7f309650037e7d0d64a8067b5b9963aefd08d504 Mon Sep 17 00:00:00 2001 From: huntharo Date: Thu, 12 Mar 2026 17:01:24 -0400 Subject: [PATCH 06/34] test: add large cluster perf comparison harness --- package.json | 1 + packages/api-core/src/cluster/perf-large.json | 31 ++++++ .../api-core/src/cluster/perf.integration.ts | 14 ++- scripts/cluster-perf-large-compare.mjs | 97 +++++++++++++++++++ 4 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 packages/api-core/src/cluster/perf-large.json create mode 100644 scripts/cluster-perf-large-compare.mjs diff --git a/package.json b/package.json index 67bacc8..fbf921b 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "serve": "node ./apps/cli/bin/ghcrawl.js serve", "project:sync": "node ./.agents/skills/project-manager/scripts/sync-work-items.mjs", "test:cluster-perf": "pnpm --filter @ghcrawl/api-core test:cluster-perf", + "perf:cluster:large": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-perf-large-compare.mjs", "pack:smoke": "node ./scripts/pack-smoke.mjs", "release:metadata": "node ./scripts/release-metadata.mjs", "release:apply-version": "node ./scripts/apply-release-version.mjs", diff --git a/packages/api-core/src/cluster/perf-large.json b/packages/api-core/src/cluster/perf-large.json new file mode 100644 index 0000000..5ea3b62 --- /dev/null +++ b/packages/api-core/src/cluster/perf-large.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "fixture": { + "clusterCount": 192, + "threadsPerCluster": 16, + "clusterBlockWidth": 4, + "noiseDimensions": 32, + "assertExactClusterCount": false, + "sourceKinds": [ + "title", + "body", + "dedupe_summary" + ], + "k": 7, + "minScore": 0.82 + }, + "benchmark": { + "warmupRuns": 0, + "runsPerSample": 1, + "minSamples": 1, + "maxSamples": 1, + "maxTotalMs": 600000 + }, + "baseline": { + "fixtureMedianMs": 0, + "projectedOpenclawMs": 600000 + }, + "thresholds": { + "maxRegressionPercent": 1000 + } +} diff --git a/packages/api-core/src/cluster/perf.integration.ts b/packages/api-core/src/cluster/perf.integration.ts index 7f45ea1..0a5ff56 100644 --- a/packages/api-core/src/cluster/perf.integration.ts +++ b/packages/api-core/src/cluster/perf.integration.ts @@ -16,6 +16,7 @@ type PerfBaseline = { threadsPerCluster: number; clusterBlockWidth: number; noiseDimensions: number; + assertExactClusterCount?: boolean; sourceKinds: EmbeddingSourceKind[]; k: number; minScore: number; @@ -60,10 +61,15 @@ type SuggestedBaseline = { projectedOpenclawMs: number; }; -const BASELINE_PATH = fileURLToPath(new URL('./perf-baseline.json', import.meta.url)); +const DEFAULT_BASELINE_PATH = fileURLToPath(new URL('./perf-baseline.json', import.meta.url)); + +function getBaselinePath(): string { + const configuredPath = process.env.GHCRAWL_CLUSTER_PERF_CONFIG_PATH?.trim(); + return configuredPath ? path.resolve(configuredPath) : DEFAULT_BASELINE_PATH; +} function loadBaseline(): PerfBaseline { - return JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8')) as PerfBaseline; + return JSON.parse(fs.readFileSync(getBaselinePath(), 'utf8')) as PerfBaseline; } function shouldBootstrapBaseline(): boolean { @@ -83,7 +89,7 @@ function assertBenchmarkShape( baseline: PerfBaseline, backend: 'exact' | 'vectorlite', ): void { - if (backend === 'exact') { + if (backend === 'exact' && baseline.fixture.assertExactClusterCount !== false) { assert.equal(result.clusters, baseline.fixture.clusterCount); } else { assert.ok(result.clusters > 0); @@ -339,7 +345,7 @@ async function measureBenchmark(baseline: PerfBaseline): Promise const backend = getPerfBackend(); if (baseline.baseline.fixtureMedianMs <= 0 && !shouldBootstrapBaseline()) { throw new Error( - `Cluster perf baseline is not set in ${BASELINE_PATH}. Run the benchmark once, then record fixtureMedianMs before enforcing regressions.`, + `Cluster perf baseline is not set in ${getBaselinePath()}. Run the benchmark once, then record fixtureMedianMs before enforcing regressions.`, ); } diff --git a/scripts/cluster-perf-large-compare.mjs b/scripts/cluster-perf-large-compare.mjs new file mode 100644 index 0000000..0e84cf1 --- /dev/null +++ b/scripts/cluster-perf-large-compare.mjs @@ -0,0 +1,97 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; + +const repoRoot = path.resolve(new URL('..', import.meta.url).pathname); +const apiCoreRoot = path.join(repoRoot, 'packages', 'api-core'); +const perfConfigPath = path.join(apiCoreRoot, 'src', 'cluster', 'perf-large.json'); +const perfEntryPath = path.join(apiCoreRoot, 'dist', 'cluster', 'perf.integration.js'); + +function formatDurationMs(durationMs) { + if (!Number.isFinite(durationMs)) return 'n/a'; + if (durationMs < 1000) return `${durationMs.toFixed(1)} ms`; + const totalSeconds = durationMs / 1000; + if (totalSeconds < 60) return `${totalSeconds.toFixed(2)} s`; + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds - minutes * 60; + return `${minutes}m ${seconds.toFixed(1)}s`; +} + +function formatPercent(value) { + const sign = value > 0 ? '+' : ''; + return `${sign}${value.toFixed(1)}%`; +} + +function runPerf({ backend, outputPath }) { + const env = { + ...process.env, + GHCRAWL_CLUSTER_PERF_BOOTSTRAP: '1', + GHCRAWL_CLUSTER_PERF_IGNORE_THRESHOLD: '1', + GHCRAWL_CLUSTER_PERF_CONFIG_PATH: perfConfigPath, + GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: outputPath, + }; + + if (backend === 'vectorlite') { + env.GHCRAWL_CLUSTER_PERF_BACKEND = 'vectorlite'; + } else { + delete env.GHCRAWL_CLUSTER_PERF_BACKEND; + } + + execFileSync(process.execPath, [perfEntryPath], { + cwd: apiCoreRoot, + env, + stdio: 'inherit', + }); + + return JSON.parse(fs.readFileSync(outputPath, 'utf8')); +} + +function main() { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ghcrawl-cluster-perf-large-')); + try { + const exactOutputPath = path.join(tempRoot, 'exact.json'); + const vectorliteOutputPaths = [1, 2, 3].map((attempt) => path.join(tempRoot, `vectorlite-${attempt}.json`)); + + const exact = runPerf({ backend: 'exact', outputPath: exactOutputPath }); + let vectorlite = null; + for (const outputPath of vectorliteOutputPaths) { + vectorlite = runPerf({ backend: 'vectorlite', outputPath }); + } + + if (!vectorlite) { + throw new Error('Vectorlite perf result was not produced.'); + } + + const exactMedianMs = exact.result.medianMs; + const vectorliteMedianMs = vectorlite.result.medianMs; + const deltaMs = vectorliteMedianMs - exactMedianMs; + const deltaPercent = exactMedianMs > 0 ? (deltaMs / exactMedianMs) * 100 : 0; + const speedup = vectorliteMedianMs > 0 ? exactMedianMs / vectorliteMedianMs : 0; + + const lines = [ + '## Large Cluster Perf Comparison', + '', + `- Fixture config: ${path.relative(repoRoot, perfConfigPath)}`, + `- Exact median (full-run): ${formatDurationMs(exactMedianMs)}`, + `- Vectorlite median (post-index, run 3/3): ${formatDurationMs(vectorliteMedianMs)}`, + `- Vectorlite delta vs exact: ${formatDurationMs(deltaMs)} (${formatPercent(deltaPercent)})`, + `- Speedup: ${speedup.toFixed(2)}x`, + '', + '### Exact Summary', + '', + exact.summary.trim(), + '', + '### Vectorlite Summary (run 3/3)', + '', + vectorlite.summary.trim(), + '', + ]; + + process.stdout.write(`${lines.join('\n')}\n`); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } +} + +main(); From 8c7ae7ec547973f6090ec8b26db57c1e899df62d Mon Sep 17 00:00:00 2001 From: huntharo Date: Thu, 12 Mar 2026 17:23:36 -0400 Subject: [PATCH 07/34] ci: use large fixture for cluster perf --- .github/workflows/ci.yml | 7 +++++++ packages/api-core/src/cluster/perf-large.json | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13fc658..99d54d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,10 @@ jobs: if: ${{ github.event_name == 'pull_request' }} run: git worktree add --detach "$RUNNER_TEMP/cluster-perf-base" "${{ github.event.pull_request.base.sha }}" + - name: Copy large cluster perf fixture into baseline worktree + if: ${{ github.event_name == 'pull_request' }} + run: cp packages/api-core/src/cluster/perf-large.json "$RUNNER_TEMP/cluster-perf-base/packages/api-core/src/cluster/perf-baseline.json" + - name: Install dependencies for baseline worktree if: ${{ github.event_name == 'pull_request' }} working-directory: ${{ runner.temp }}/cluster-perf-base @@ -91,6 +95,7 @@ jobs: - name: Run PR head exact cluster perf env: + GHCRAWL_CLUSTER_PERF_CONFIG_PATH: ${{ github.workspace }}/packages/api-core/src/cluster/perf-large.json GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf-head-exact.json run: pnpm test:cluster-perf @@ -98,6 +103,7 @@ jobs: if: ${{ github.event_name == 'pull_request' }} env: GHCRAWL_CLUSTER_PERF_BACKEND: vectorlite + GHCRAWL_CLUSTER_PERF_CONFIG_PATH: ${{ github.workspace }}/packages/api-core/src/cluster/perf-large.json GHCRAWL_CLUSTER_PERF_IGNORE_THRESHOLD: 1 GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf-head-vectorlite.json run: | @@ -151,6 +157,7 @@ jobs: const lines = [ '## Cluster Performance', '', + `- Fixture shape: ${headExact.result.threadCount} threads x ${headExact.result.sourceKinds.length} source kinds`, `- Head exact fixture median (full-run): ${formatDurationMs(headExact.result.medianMs)}`, `- Head exact projected openclaw/openclaw: ${formatDurationMs(headExact.result.projectedOpenclawMs)}`, ]; diff --git a/packages/api-core/src/cluster/perf-large.json b/packages/api-core/src/cluster/perf-large.json index 5ea3b62..abae01f 100644 --- a/packages/api-core/src/cluster/perf-large.json +++ b/packages/api-core/src/cluster/perf-large.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "fixture": { - "clusterCount": 192, + "clusterCount": 96, "threadsPerCluster": 16, "clusterBlockWidth": 4, "noiseDimensions": 32, @@ -22,7 +22,7 @@ "maxTotalMs": 600000 }, "baseline": { - "fixtureMedianMs": 0, + "fixtureMedianMs": 1800, "projectedOpenclawMs": 600000 }, "thresholds": { From aa368f743b3967760a553bacd2c79be973b5fd95 Mon Sep 17 00:00:00 2001 From: huntharo Date: Thu, 12 Mar 2026 17:25:24 -0400 Subject: [PATCH 08/34] ci: reuse current perf harness for baseline worktree --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99d54d2..8a5264d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,9 @@ jobs: - name: Copy large cluster perf fixture into baseline worktree if: ${{ github.event_name == 'pull_request' }} - run: cp packages/api-core/src/cluster/perf-large.json "$RUNNER_TEMP/cluster-perf-base/packages/api-core/src/cluster/perf-baseline.json" + run: | + cp packages/api-core/src/cluster/perf-large.json "$RUNNER_TEMP/cluster-perf-base/packages/api-core/src/cluster/perf-baseline.json" + cp packages/api-core/src/cluster/perf.integration.ts "$RUNNER_TEMP/cluster-perf-base/packages/api-core/src/cluster/perf.integration.ts" - name: Install dependencies for baseline worktree if: ${{ github.event_name == 'pull_request' }} From a3800f4804acff8fb72ebb56dca3a2c2c2dea44b Mon Sep 17 00:00:00 2001 From: huntharo Date: Sat, 28 Mar 2026 20:54:20 -0400 Subject: [PATCH 09/34] fix: restore vectorlite build after rebase --- packages/api-core/src/service.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index 7480088..6b13029 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -2995,6 +2995,19 @@ export class GHCrawlService { .all(repoId, this.config.embedModel) as StoredEmbeddingRow[]; } + private loadParsedStoredEmbeddings(repoId: number): ParsedStoredEmbeddingRow[] { + return this.loadStoredEmbeddings(repoId).map((row) => { + const embedding = JSON.parse(row.embedding_json) as number[]; + const normalized = normalizeEmbedding(embedding); + return { + ...row, + embedding, + normalizedEmbedding: normalized.normalized, + embeddingNorm: normalized.norm, + }; + }); + } + private loadStoredEmbeddingsForThreadNumber(repoId: number, threadNumber: number): StoredEmbeddingRow[] { return this.db .prepare( From a9fda34a9b39244b3add318cdf9843a8604fdfac Mon Sep 17 00:00:00 2001 From: huntharo Date: Sat, 28 Mar 2026 21:08:20 -0400 Subject: [PATCH 10/34] fix: measure cluster perf samples from run duration --- packages/api-core/src/cluster/perf.integration.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/api-core/src/cluster/perf.integration.ts b/packages/api-core/src/cluster/perf.integration.ts index 0a5ff56..daefe58 100644 --- a/packages/api-core/src/cluster/perf.integration.ts +++ b/packages/api-core/src/cluster/perf.integration.ts @@ -368,15 +368,16 @@ async function measureBenchmark(baseline: PerfBaseline): Promise } while (sampleDurationsMs.length < baseline.benchmark.maxSamples) { - const sampleStartedAt = performance.now(); + let sampleDurationMs = 0; for (let runIndex = 0; runIndex < runsPerSample; runIndex += 1) { const runDbPath = path.join(tempRoot, `run-${runCounter}.sqlite`); runCounter += 1; fs.copyFileSync(seedDbPath, runDbPath); const result = await runSingleCluster(runDbPath, baseline, backend); assertBenchmarkShape(result, baseline, backend); + sampleDurationMs += result.durationMs; } - sampleDurationsMs.push(performance.now() - sampleStartedAt); + sampleDurationsMs.push(sampleDurationMs); const elapsedMs = performance.now() - benchmarkStartedAt; if (sampleDurationsMs.length >= baseline.benchmark.minSamples && elapsedMs >= baseline.benchmark.maxTotalMs) { From ec4ead0548957c5852a58579a08251a9cbce5ce8 Mon Sep 17 00:00:00 2001 From: huntharo Date: Sat, 28 Mar 2026 21:21:46 -0400 Subject: [PATCH 11/34] feat: break down cluster perf experiment metrics --- .github/workflows/ci.yml | 24 +- apps/cli/src/main.ts | 2 +- .../api-core/src/cluster/perf.integration.ts | 151 +++++++-- packages/api-core/src/service.ts | 293 ++++++++++++------ scripts/cluster-perf-large-compare.mjs | 22 +- 5 files changed, 365 insertions(+), 127 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a5264d..18eb5d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,6 +142,12 @@ jobs: return `${minutes}m ${seconds.toFixed(1)}s`; } + function formatBytes(bytes) { + if (!Number.isFinite(bytes)) return 'n/a'; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`; + } + function formatPercent(value) { const sign = value > 0 ? '+' : ''; return `${sign}${value.toFixed(1)}%`; @@ -160,7 +166,12 @@ jobs: '## Cluster Performance', '', `- Fixture shape: ${headExact.result.threadCount} threads x ${headExact.result.sourceKinds.length} source kinds`, - `- Head exact fixture median (full-run): ${formatDurationMs(headExact.result.medianMs)}`, + `- Head exact fixture median (cluster-only): ${formatDurationMs(headExact.result.medianMs)}`, + `- Head exact fixture median (total run): ${formatDurationMs(headExact.result.totalMedianMs)}`, + `- Head exact edge-build median: ${formatDurationMs(headExact.result.edgeBuildMedianMs)}`, + `- Head exact cluster-assembly median: ${formatDurationMs(headExact.result.clusterBuildMedianMs)}`, + `- Head exact median peak RSS: ${formatBytes(headExact.result.medianPeakRssBytes)}`, + `- Head exact median peak heap used: ${formatBytes(headExact.result.medianPeakHeapUsedBytes)}`, `- Head exact projected openclaw/openclaw: ${formatDurationMs(headExact.result.projectedOpenclawMs)}`, ]; @@ -171,7 +182,7 @@ jobs: const exactProjectedDeltaPercent = base.result.projectedOpenclawMs > 0 ? (exactProjectedDeltaMs / base.result.projectedOpenclawMs) * 100 : 0; lines.push( - `- Base exact fixture median (full-run): ${formatDurationMs(base.result.medianMs)}`, + `- Base exact fixture median (cluster-only): ${formatDurationMs(base.result.medianMs)}`, `- Exact delta vs base: ${formatDurationMs(exactDeltaMs)} (${formatPercent(exactDeltaPercent)})`, `- Base exact projected openclaw/openclaw: ${formatDurationMs(base.result.projectedOpenclawMs)}`, `- Exact projected delta vs base: ${formatDurationMs(exactProjectedDeltaMs)} (${formatPercent(exactProjectedDeltaPercent)})`, @@ -187,7 +198,14 @@ jobs: headExact.result.projectedOpenclawMs > 0 ? (vectorliteProjectedDeltaMs / headExact.result.projectedOpenclawMs) * 100 : 0; lines.push( '', - `- Head vectorlite fixture median (post-index, run 3/3): ${formatDurationMs(headVectorlite.result.medianMs)}`, + `- Head vectorlite fixture median (cluster-only, run 3/3): ${formatDurationMs(headVectorlite.result.medianMs)}`, + `- Head vectorlite fixture median (total run, run 3/3): ${formatDurationMs(headVectorlite.result.totalMedianMs)}`, + `- Head vectorlite setup median: ${formatDurationMs(headVectorlite.result.setupMedianMs)}`, + `- Head vectorlite index-build median: ${formatDurationMs(headVectorlite.result.indexBuildMedianMs)}`, + `- Head vectorlite query median: ${formatDurationMs(headVectorlite.result.queryMedianMs)}`, + `- Head vectorlite cluster-assembly median: ${formatDurationMs(headVectorlite.result.clusterBuildMedianMs)}`, + `- Head vectorlite median peak RSS: ${formatBytes(headVectorlite.result.medianPeakRssBytes)}`, + `- Head vectorlite median peak heap used: ${formatBytes(headVectorlite.result.medianPeakHeapUsedBytes)}`, `- Vectorlite delta vs head exact: ${formatDurationMs(vectorliteDeltaMs)} (${formatPercent(vectorliteDeltaPercent)})`, `- Head vectorlite projected openclaw/openclaw: ${formatDurationMs(headVectorlite.result.projectedOpenclawMs)}`, `- Vectorlite projected delta vs head exact: ${formatDurationMs(vectorliteProjectedDeltaMs)} (${formatPercent(vectorliteProjectedDeltaPercent)})`, diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts index 6cc9087..928b2ef 100644 --- a/apps/cli/src/main.ts +++ b/apps/cli/src/main.ts @@ -939,7 +939,7 @@ export async function run( } case 'cluster-experiment': { const { owner, repo, values } = parseRepoFlags(rest); - const backend = values.backend === 'vectorlite' ? values.backend : undefined; + const backend = values.backend === 'exact' || values.backend === 'vectorlite' ? values.backend : undefined; const result = getService().clusterExperiment({ owner, repo, diff --git a/packages/api-core/src/cluster/perf.integration.ts b/packages/api-core/src/cluster/perf.integration.ts index daefe58..7b48698 100644 --- a/packages/api-core/src/cluster/perf.integration.ts +++ b/packages/api-core/src/cluster/perf.integration.ts @@ -39,9 +39,27 @@ type PerfBaseline = { type PerfRunResult = { backend: 'exact' | 'vectorlite'; - timingBasis: 'full-run' | 'post-index'; + timingBasis: 'cluster-only'; sampleDurationsMs: number[]; + totalSampleDurationsMs: number[]; + loadSampleDurationsMs: number[]; + setupSampleDurationsMs: number[]; + edgeBuildSampleDurationsMs: number[]; + indexBuildSampleDurationsMs: number[]; + querySampleDurationsMs: number[]; + clusterBuildSampleDurationsMs: number[]; + peakRssBytesSamples: number[]; + peakHeapUsedBytesSamples: number[]; medianMs: number; + totalMedianMs: number; + loadMedianMs: number; + setupMedianMs: number; + edgeBuildMedianMs: number; + indexBuildMedianMs: number; + queryMedianMs: number; + clusterBuildMedianMs: number; + medianPeakRssBytes: number; + medianPeakHeapUsedBytes: number; baselineMedianMs: number; deltaMs: number; deltaPercent: number; @@ -111,6 +129,14 @@ function formatDurationMs(durationMs: number): string { return `${minutes}m ${seconds.toFixed(1)}s`; } +function formatBytes(bytes: number): string { + if (!Number.isFinite(bytes)) return 'n/a'; + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(1)} KiB`; + } + return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`; +} + function formatPercent(value: number): string { const sign = value > 0 ? '+' : ''; return `${sign}${value.toFixed(1)}%`; @@ -312,30 +338,43 @@ async function runSingleCluster( dbPath: string, baseline: PerfBaseline, backend: 'exact' | 'vectorlite', -): Promise<{ durationMs: number; clusters: number; edges: number }> { +): Promise<{ + durationMs: number; + totalDurationMs: number; + loadMs: number; + setupMs: number; + edgeBuildMs: number; + indexBuildMs: number; + queryMs: number; + clusterBuildMs: number; + peakRssBytes: number; + peakHeapUsedBytes: number; + clusters: number; + edges: number; +}> { const service = createService(dbPath); try { - const startedAt = performance.now(); - if (backend === 'vectorlite') { - const result = service.clusterExperiment({ - owner: 'openclaw', - repo: 'openclaw', - backend: 'vectorlite', - k: baseline.fixture.k, - minScore: baseline.fixture.minScore, - }); - const durationMs = Math.max(0, result.durationMs - result.indexBuildMs); - return { durationMs, clusters: result.clusters, edges: result.edges }; - } - - const result = await service.clusterRepository({ + const result = service.clusterExperiment({ owner: 'openclaw', repo: 'openclaw', + backend, k: baseline.fixture.k, minScore: baseline.fixture.minScore, }); - const durationMs = performance.now() - startedAt; - return { durationMs, clusters: result.clusters, edges: result.edges }; + return { + durationMs: result.durationMs, + totalDurationMs: result.totalDurationMs, + loadMs: result.loadMs, + setupMs: result.setupMs, + edgeBuildMs: result.edgeBuildMs, + indexBuildMs: result.indexBuildMs, + queryMs: result.queryMs, + clusterBuildMs: result.clusterBuildMs, + peakRssBytes: result.memory.peakRssBytes, + peakHeapUsedBytes: result.memory.peakHeapUsedBytes, + clusters: result.clusters, + edges: result.edges, + }; } finally { service.close(); } @@ -357,6 +396,15 @@ async function measureBenchmark(baseline: PerfBaseline): Promise const warmupRuns = baseline.benchmark.warmupRuns; const runsPerSample = baseline.benchmark.runsPerSample; const sampleDurationsMs: number[] = []; + const totalSampleDurationsMs: number[] = []; + const loadSampleDurationsMs: number[] = []; + const setupSampleDurationsMs: number[] = []; + const edgeBuildSampleDurationsMs: number[] = []; + const indexBuildSampleDurationsMs: number[] = []; + const querySampleDurationsMs: number[] = []; + const clusterBuildSampleDurationsMs: number[] = []; + const peakRssBytesSamples: number[] = []; + const peakHeapUsedBytesSamples: number[] = []; const benchmarkStartedAt = performance.now(); let runCounter = 0; @@ -369,6 +417,15 @@ async function measureBenchmark(baseline: PerfBaseline): Promise while (sampleDurationsMs.length < baseline.benchmark.maxSamples) { let sampleDurationMs = 0; + let totalSampleDurationMs = 0; + let loadSampleDurationMs = 0; + let setupSampleDurationMs = 0; + let edgeBuildSampleDurationMs = 0; + let indexBuildSampleDurationMs = 0; + let querySampleDurationMs = 0; + let clusterBuildSampleDurationMs = 0; + let samplePeakRssBytes = 0; + let samplePeakHeapUsedBytes = 0; for (let runIndex = 0; runIndex < runsPerSample; runIndex += 1) { const runDbPath = path.join(tempRoot, `run-${runCounter}.sqlite`); runCounter += 1; @@ -376,8 +433,26 @@ async function measureBenchmark(baseline: PerfBaseline): Promise const result = await runSingleCluster(runDbPath, baseline, backend); assertBenchmarkShape(result, baseline, backend); sampleDurationMs += result.durationMs; + totalSampleDurationMs += result.totalDurationMs; + loadSampleDurationMs += result.loadMs; + setupSampleDurationMs += result.setupMs; + edgeBuildSampleDurationMs += result.edgeBuildMs; + indexBuildSampleDurationMs += result.indexBuildMs; + querySampleDurationMs += result.queryMs; + clusterBuildSampleDurationMs += result.clusterBuildMs; + samplePeakRssBytes = Math.max(samplePeakRssBytes, result.peakRssBytes); + samplePeakHeapUsedBytes = Math.max(samplePeakHeapUsedBytes, result.peakHeapUsedBytes); } sampleDurationsMs.push(sampleDurationMs); + totalSampleDurationsMs.push(totalSampleDurationMs); + loadSampleDurationsMs.push(loadSampleDurationMs); + setupSampleDurationsMs.push(setupSampleDurationMs); + edgeBuildSampleDurationsMs.push(edgeBuildSampleDurationMs); + indexBuildSampleDurationsMs.push(indexBuildSampleDurationMs); + querySampleDurationsMs.push(querySampleDurationMs); + clusterBuildSampleDurationsMs.push(clusterBuildSampleDurationMs); + peakRssBytesSamples.push(samplePeakRssBytes); + peakHeapUsedBytesSamples.push(samplePeakHeapUsedBytes); const elapsedMs = performance.now() - benchmarkStartedAt; if (sampleDurationsMs.length >= baseline.benchmark.minSamples && elapsedMs >= baseline.benchmark.maxTotalMs) { @@ -386,6 +461,15 @@ async function measureBenchmark(baseline: PerfBaseline): Promise } const medianMs = median(sampleDurationsMs); + const totalMedianMs = median(totalSampleDurationsMs); + const loadMedianMs = median(loadSampleDurationsMs); + const setupMedianMs = median(setupSampleDurationsMs); + const edgeBuildMedianMs = median(edgeBuildSampleDurationsMs); + const indexBuildMedianMs = median(indexBuildSampleDurationsMs); + const queryMedianMs = median(querySampleDurationsMs); + const clusterBuildMedianMs = median(clusterBuildSampleDurationsMs); + const medianPeakRssBytes = median(peakRssBytesSamples); + const medianPeakHeapUsedBytes = median(peakHeapUsedBytesSamples); const baselineMedianMs = baseline.baseline.fixtureMedianMs > 0 ? baseline.baseline.fixtureMedianMs : medianMs; const deltaMs = medianMs - baselineMedianMs; const deltaPercent = baselineMedianMs > 0 ? (deltaMs / baselineMedianMs) * 100 : 0; @@ -396,9 +480,27 @@ async function measureBenchmark(baseline: PerfBaseline): Promise return { backend, - timingBasis: backend === 'vectorlite' ? 'post-index' : 'full-run', + timingBasis: 'cluster-only', sampleDurationsMs, + totalSampleDurationsMs, + loadSampleDurationsMs, + setupSampleDurationsMs, + edgeBuildSampleDurationsMs, + indexBuildSampleDurationsMs, + querySampleDurationsMs, + clusterBuildSampleDurationsMs, + peakRssBytesSamples, + peakHeapUsedBytesSamples, medianMs, + totalMedianMs, + loadMedianMs, + setupMedianMs, + edgeBuildMedianMs, + indexBuildMedianMs, + queryMedianMs, + clusterBuildMedianMs, + medianPeakRssBytes, + medianPeakHeapUsedBytes, baselineMedianMs, deltaMs, deltaPercent, @@ -435,7 +537,16 @@ function buildSummary(result: PerfRunResult): string { `- Backend: ${result.backend}`, `- Timing basis: ${result.timingBasis}`, `- Status: ${status}`, - `- ${timingLabel}: ${formatDurationMs(result.medianMs)} (${result.samples} samples, ${result.runsPerSample} cluster rebuilds/sample)`, + `- Fixture median (cluster-only): ${formatDurationMs(result.medianMs)} (${result.samples} samples, ${result.runsPerSample} cluster rebuilds/sample)`, + `- Fixture median (total run): ${formatDurationMs(result.totalMedianMs)}`, + `- Fixture median load stage: ${formatDurationMs(result.loadMedianMs)}`, + `- Fixture median setup stage: ${formatDurationMs(result.setupMedianMs)}`, + `- Fixture median exact edge-build stage: ${formatDurationMs(result.edgeBuildMedianMs)}`, + `- Fixture median vector index-build stage: ${formatDurationMs(result.indexBuildMedianMs)}`, + `- Fixture median vector query stage: ${formatDurationMs(result.queryMedianMs)}`, + `- Fixture median cluster-assembly stage: ${formatDurationMs(result.clusterBuildMedianMs)}`, + `- Median peak RSS: ${formatBytes(result.medianPeakRssBytes)}`, + `- Median peak heap used: ${formatBytes(result.medianPeakHeapUsedBytes)}`, `- Fixture baseline: ${formatDurationMs(result.baselineMedianMs)}`, `- Fixture delta: ${formatDurationMs(result.deltaMs)} (${formatPercent(result.deltaPercent)})`, `- Projected openclaw/openclaw duration: ${formatDurationMs(result.projectedOpenclawMs)}`, diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index 6b13029..9827c80 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -123,18 +123,34 @@ type ParsedStoredEmbeddingRow = Omit & { embeddingNorm: number; }; +type ClusterExperimentMemoryStats = { + rssBeforeBytes: number; + rssAfterBytes: number; + peakRssBytes: number; + heapUsedBeforeBytes: number; + heapUsedAfterBytes: number; + peakHeapUsedBytes: number; +}; + type ClusterExperimentResult = { - backend: 'vectorlite'; + backend: 'exact' | 'vectorlite'; repository: RepositoryDto; - tempDbPath: string; + tempDbPath: string | null; threads: number; sourceKinds: number; edges: number; clusters: number; + timingBasis: 'cluster-only'; durationMs: number; + totalDurationMs: number; + loadMs: number; + setupMs: number; + edgeBuildMs: number; indexBuildMs: number; queryMs: number; + clusterBuildMs: number; candidateK: number; + memory: ClusterExperimentMemoryStats; }; type EmbeddingWorkset = { rows: Array<{ @@ -1147,132 +1163,183 @@ export class GHCrawlService { clusterExperiment(params: { owner: string; repo: string; - backend?: 'vectorlite'; + backend?: 'exact' | 'vectorlite'; minScore?: number; k?: number; candidateK?: number; onProgress?: (message: string) => void; }): ClusterExperimentResult { const backend = params.backend ?? 'vectorlite'; - if (backend !== 'vectorlite') { - throw new Error(`Unsupported experimental cluster backend: ${backend}`); - } - const repository = this.requireRepository(params.owner, params.repo); const { items, sourceKinds } = this.loadClusterableThreadMeta(repository.id); const minScore = params.minScore ?? 0.82; const k = params.k ?? 6; const candidateK = Math.max(k, params.candidateK ?? Math.max(k * 16, 64)); + const startedAt = Date.now(); + const memoryBefore = process.memoryUsage(); + let peakRssBytes = memoryBefore.rss; + let peakHeapUsedBytes = memoryBefore.heapUsed; + const recordMemory = (): void => { + const usage = process.memoryUsage(); + peakRssBytes = Math.max(peakRssBytes, usage.rss); + peakHeapUsedBytes = Math.max(peakHeapUsedBytes, usage.heapUsed); + }; + recordMemory(); params.onProgress?.( `[cluster-experiment] loaded ${items.length} embedded thread(s) across ${sourceKinds.length} source kind(s) for ${repository.fullName} backend=${backend} k=${k} candidateK=${candidateK} minScore=${minScore}`, ); - const rows = this.loadParsedStoredEmbeddings(repository.id); - const bySource = new Map(); - for (const row of rows) { - const list = bySource.get(row.source_kind) ?? []; - list.push(row); - bySource.set(row.source_kind, list); - } - - const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghcrawl-vectorlite-')); - const tempDbPath = path.join(tempDir, 'cluster-experiment.db'); - const tempDb = openDb(tempDbPath); + const aggregated = new Map }>(); + let loadMs = 0; + let setupMs = 0; + let edgeBuildMs = 0; let indexBuildMs = 0; let queryMs = 0; + let clusterBuildMs = 0; + let tempDbPath: string | null = null; + let tempDb: SqliteDatabase | null = null; + let tempDir: string | null = null; try { - tempDb.pragma('journal_mode = MEMORY'); - tempDb.pragma('synchronous = OFF'); - tempDb.pragma('temp_store = MEMORY'); - const vectorlite = requireFromHere('vectorlite') as { vectorlitePath: () => string }; - (tempDb as SqliteDatabase & { loadExtension: (extensionPath: string) => void }).loadExtension(vectorlite.vectorlitePath()); - - const experimentStartedAt = Date.now(); - const aggregated = new Map }>(); - - for (const sourceKind of sourceKinds) { - const sourceRows = bySource.get(sourceKind) ?? []; - if (sourceRows.length === 0) { - continue; - } - - const tableName = `vector_${sourceKind}`; - const dimension = sourceRows[0]?.normalizedEmbedding.length ?? 0; - const safeCandidateK = Math.min(candidateK, Math.max(1, sourceRows.length - 1)); - - params.onProgress?.( - `[cluster-experiment] building ${sourceKind} HNSW index with ${sourceRows.length} vector(s)`, - ); - const indexStartedAt = Date.now(); - tempDb.exec( - `create virtual table ${tableName} using vectorlite(vec float32[${dimension}], hnsw(max_elements=${sourceRows.length}));`, - ); - const insert = tempDb.prepare(`insert into ${tableName}(rowid, vec) values (?, ?)`); - tempDb.transaction(() => { - for (const row of sourceRows) { - insert.run(row.id, this.normalizedEmbeddingBuffer(row.normalizedEmbedding)); - } - })(); - indexBuildMs += Date.now() - indexStartedAt; - - const rowsById = new Map(sourceRows.map((row) => [row.id, row])); - const queryStartedAt = Date.now(); - const query = tempDb.prepare( - `select rowid from ${tableName} where knn_search(vec, knn_param(?, ${safeCandidateK + 1}))`, - ); - let processed = 0; - let lastProgressAt = Date.now(); - for (const row of sourceRows) { - const candidates = query.all(this.normalizedEmbeddingBuffer(row.normalizedEmbedding)) as Array<{ rowid: number }>; - const ranked = rankNearestNeighborsByScore(candidates, { + if (backend === 'exact') { + const totalItems = sourceKinds.reduce((sum, sourceKind) => sum + this.countEmbeddingsForSourceKind(repository.id, sourceKind), 0); + let processedItems = 0; + + for (const sourceKind of sourceKinds) { + const loadStartedAt = Date.now(); + const normalizedRows = this.loadNormalizedEmbeddingsForSourceKind(repository.id, sourceKind); + loadMs += Date.now() - loadStartedAt; + recordMemory(); + + const edgesStartedAt = Date.now(); + const edges = buildSourceKindEdges(normalizedRows, { limit: k, minScore, - score: (candidate) => { - if (candidate.rowid === row.id) { - return -1; - } - const candidateRow = rowsById.get(candidate.rowid); - if (!candidateRow) { - return -1; - } - return dotProduct(row.normalizedEmbedding, candidateRow.normalizedEmbedding); + progressIntervalMs: CLUSTER_PROGRESS_INTERVAL_MS, + onProgress: (progress) => { + recordMemory(); + if (!params.onProgress) return; + params.onProgress( + `[cluster-experiment] exact ${processedItems + progress.processedItems}/${totalItems} source embeddings processed current_edges~=${aggregated.size + progress.currentEdgeEstimate}`, + ); }, }); - let addedThisRow = 0; - for (const candidate of ranked) { - const candidateRow = rowsById.get(candidate.item.rowid); - if (!candidateRow) continue; - const score = candidate.score; - const key = this.edgeKey(row.id, candidateRow.id); - const existing = aggregated.get(key); - if (existing) { - existing.score = Math.max(existing.score, score); - existing.sourceKinds.add(sourceKind); - continue; + edgeBuildMs += Date.now() - edgesStartedAt; + processedItems += normalizedRows.length; + this.mergeSourceKindEdges(aggregated, edges, sourceKind); + recordMemory(); + } + } else { + const loadStartedAt = Date.now(); + const rows = this.loadParsedStoredEmbeddings(repository.id); + const bySource = new Map(); + for (const row of rows) { + const list = bySource.get(row.source_kind) ?? []; + list.push(row); + bySource.set(row.source_kind, list); + } + loadMs += Date.now() - loadStartedAt; + recordMemory(); + + const setupStartedAt = Date.now(); + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghcrawl-vectorlite-')); + tempDbPath = path.join(tempDir, 'cluster-experiment.db'); + tempDb = openDb(tempDbPath); + tempDb.pragma('journal_mode = MEMORY'); + tempDb.pragma('synchronous = OFF'); + tempDb.pragma('temp_store = MEMORY'); + const vectorlite = requireFromHere('vectorlite') as { vectorlitePath: () => string }; + (tempDb as SqliteDatabase & { loadExtension: (extensionPath: string) => void }).loadExtension(vectorlite.vectorlitePath()); + setupMs += Date.now() - setupStartedAt; + recordMemory(); + + for (const sourceKind of sourceKinds) { + const sourceRows = bySource.get(sourceKind) ?? []; + if (sourceRows.length === 0) { + continue; + } + + const tableName = `vector_${sourceKind}`; + const dimension = sourceRows[0]?.normalizedEmbedding.length ?? 0; + const safeCandidateK = Math.min(candidateK, Math.max(1, sourceRows.length - 1)); + + params.onProgress?.( + `[cluster-experiment] building ${sourceKind} HNSW index with ${sourceRows.length} vector(s)`, + ); + const indexStartedAt = Date.now(); + tempDb.exec( + `create virtual table ${tableName} using vectorlite(vec float32[${dimension}], hnsw(max_elements=${sourceRows.length}));`, + ); + const insert = tempDb.prepare(`insert into ${tableName}(rowid, vec) values (?, ?)`); + tempDb.transaction(() => { + for (const row of sourceRows) { + insert.run(row.id, this.normalizedEmbeddingBuffer(row.normalizedEmbedding)); } - aggregated.set(key, { - leftThreadId: Math.min(row.id, candidateRow.id), - rightThreadId: Math.max(row.id, candidateRow.id), - score, - sourceKinds: new Set([sourceKind]), + })(); + indexBuildMs += Date.now() - indexStartedAt; + recordMemory(); + + const rowsById = new Map(sourceRows.map((row) => [row.id, row])); + const queryStartedAt = Date.now(); + const query = tempDb.prepare( + `select rowid from ${tableName} where knn_search(vec, knn_param(?, ${safeCandidateK + 1}))`, + ); + let processed = 0; + let lastProgressAt = Date.now(); + for (const row of sourceRows) { + const candidates = query.all(this.normalizedEmbeddingBuffer(row.normalizedEmbedding)) as Array<{ rowid: number }>; + const ranked = rankNearestNeighborsByScore(candidates, { + limit: k, + minScore, + score: (candidate) => { + if (candidate.rowid === row.id) { + return -1; + } + const candidateRow = rowsById.get(candidate.rowid); + if (!candidateRow) { + return -1; + } + return dotProduct(row.normalizedEmbedding, candidateRow.normalizedEmbedding); + }, }); - addedThisRow += 1; - } - processed += 1; - const now = Date.now(); - if (params.onProgress && now - lastProgressAt >= CLUSTER_PROGRESS_INTERVAL_MS) { - params.onProgress( - `[cluster-experiment] querying ${sourceKind} index ${processed}/${sourceRows.length} current_edges=${aggregated.size} added_this_step=${addedThisRow}`, - ); - lastProgressAt = now; + let addedThisRow = 0; + for (const candidate of ranked) { + const candidateRow = rowsById.get(candidate.item.rowid); + if (!candidateRow) continue; + const score = candidate.score; + const key = this.edgeKey(row.id, candidateRow.id); + const existing = aggregated.get(key); + if (existing) { + existing.score = Math.max(existing.score, score); + existing.sourceKinds.add(sourceKind); + continue; + } + aggregated.set(key, { + leftThreadId: Math.min(row.id, candidateRow.id), + rightThreadId: Math.max(row.id, candidateRow.id), + score, + sourceKinds: new Set([sourceKind]), + }); + addedThisRow += 1; + } + processed += 1; + const now = Date.now(); + if (params.onProgress && now - lastProgressAt >= CLUSTER_PROGRESS_INTERVAL_MS) { + recordMemory(); + params.onProgress( + `[cluster-experiment] querying ${sourceKind} index ${processed}/${sourceRows.length} current_edges=${aggregated.size} added_this_step=${addedThisRow}`, + ); + lastProgressAt = now; + } } + queryMs += Date.now() - queryStartedAt; + tempDb.exec(`drop table ${tableName}`); + recordMemory(); } - queryMs += Date.now() - queryStartedAt; - tempDb.exec(`drop table ${tableName}`); } + const clusterStartedAt = Date.now(); const clusters = buildClusters( items.map((item) => ({ threadId: item.id, number: item.number, title: item.title })), Array.from(aggregated.values()).map((entry) => ({ @@ -1281,6 +1348,14 @@ export class GHCrawlService { score: entry.score, })), ); + clusterBuildMs += Date.now() - clusterStartedAt; + recordMemory(); + const memoryAfter = process.memoryUsage(); + const durationMs = + backend === 'vectorlite' + ? indexBuildMs + queryMs + clusterBuildMs + : edgeBuildMs + clusterBuildMs; + const totalDurationMs = Date.now() - startedAt; return { backend, @@ -1290,14 +1365,30 @@ export class GHCrawlService { sourceKinds: sourceKinds.length, edges: aggregated.size, clusters: clusters.length, - durationMs: Date.now() - experimentStartedAt, + timingBasis: 'cluster-only', + durationMs, + totalDurationMs, + loadMs, + setupMs, + edgeBuildMs, indexBuildMs, queryMs, + clusterBuildMs, candidateK, + memory: { + rssBeforeBytes: memoryBefore.rss, + rssAfterBytes: memoryAfter.rss, + peakRssBytes, + heapUsedBeforeBytes: memoryBefore.heapUsed, + heapUsedAfterBytes: memoryAfter.heapUsed, + peakHeapUsedBytes, + }, }; } finally { - tempDb.close(); - fs.rmSync(tempDir, { recursive: true, force: true }); + tempDb?.close(); + if (tempDir) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } } } diff --git a/scripts/cluster-perf-large-compare.mjs b/scripts/cluster-perf-large-compare.mjs index 0e84cf1..20fdf39 100644 --- a/scripts/cluster-perf-large-compare.mjs +++ b/scripts/cluster-perf-large-compare.mjs @@ -23,6 +23,12 @@ function formatPercent(value) { return `${sign}${value.toFixed(1)}%`; } +function formatBytes(bytes) { + if (!Number.isFinite(bytes)) return 'n/a'; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`; +} + function runPerf({ backend, outputPath }) { const env = { ...process.env, @@ -73,8 +79,20 @@ function main() { '## Large Cluster Perf Comparison', '', `- Fixture config: ${path.relative(repoRoot, perfConfigPath)}`, - `- Exact median (full-run): ${formatDurationMs(exactMedianMs)}`, - `- Vectorlite median (post-index, run 3/3): ${formatDurationMs(vectorliteMedianMs)}`, + `- Exact median (cluster-only): ${formatDurationMs(exactMedianMs)}`, + `- Exact median (total run): ${formatDurationMs(exact.result.totalMedianMs)}`, + `- Exact edge-build median: ${formatDurationMs(exact.result.edgeBuildMedianMs)}`, + `- Exact cluster-assembly median: ${formatDurationMs(exact.result.clusterBuildMedianMs)}`, + `- Exact median peak RSS: ${formatBytes(exact.result.medianPeakRssBytes)}`, + `- Exact median peak heap used: ${formatBytes(exact.result.medianPeakHeapUsedBytes)}`, + `- Vectorlite median (cluster-only, run 3/3): ${formatDurationMs(vectorliteMedianMs)}`, + `- Vectorlite median (total run, run 3/3): ${formatDurationMs(vectorlite.result.totalMedianMs)}`, + `- Vectorlite setup median: ${formatDurationMs(vectorlite.result.setupMedianMs)}`, + `- Vectorlite index-build median: ${formatDurationMs(vectorlite.result.indexBuildMedianMs)}`, + `- Vectorlite query median: ${formatDurationMs(vectorlite.result.queryMedianMs)}`, + `- Vectorlite cluster-assembly median: ${formatDurationMs(vectorlite.result.clusterBuildMedianMs)}`, + `- Vectorlite median peak RSS: ${formatBytes(vectorlite.result.medianPeakRssBytes)}`, + `- Vectorlite median peak heap used: ${formatBytes(vectorlite.result.medianPeakHeapUsedBytes)}`, `- Vectorlite delta vs exact: ${formatDurationMs(deltaMs)} (${formatPercent(deltaPercent)})`, `- Speedup: ${speedup.toFixed(2)}x`, '', From a5bc24a94fd51df63c38fbea9b9e5f954599ef37 Mon Sep 17 00:00:00 2001 From: huntharo Date: Sat, 28 Mar 2026 22:06:58 -0400 Subject: [PATCH 12/34] feat: add real-db cluster perf benchmark --- package.json | 1 + scripts/cluster-perf-real-compare.mjs | 320 ++++++++++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 scripts/cluster-perf-real-compare.mjs diff --git a/package.json b/package.json index fbf921b..7856296 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "project:sync": "node ./.agents/skills/project-manager/scripts/sync-work-items.mjs", "test:cluster-perf": "pnpm --filter @ghcrawl/api-core test:cluster-perf", "perf:cluster:large": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-perf-large-compare.mjs", + "perf:cluster:real": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-perf-real-compare.mjs", "pack:smoke": "node ./scripts/pack-smoke.mjs", "release:metadata": "node ./scripts/release-metadata.mjs", "release:apply-version": "node ./scripts/apply-release-version.mjs", diff --git a/scripts/cluster-perf-real-compare.mjs b/scripts/cluster-perf-real-compare.mjs new file mode 100644 index 0000000..d833cc3 --- /dev/null +++ b/scripts/cluster-perf-real-compare.mjs @@ -0,0 +1,320 @@ +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import readline from 'node:readline'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const serviceModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'service.js'); + +const { GHCrawlService } = await import(serviceModulePath); + +function formatDurationMs(durationMs) { + if (!Number.isFinite(durationMs)) return 'n/a'; + if (durationMs < 1000) return `${durationMs.toFixed(1)} ms`; + const totalSeconds = durationMs / 1000; + if (totalSeconds < 60) return `${totalSeconds.toFixed(2)} s`; + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds - minutes * 60; + return `${minutes}m ${seconds.toFixed(1)}s`; +} + +function formatBytes(bytes) { + if (!Number.isFinite(bytes)) return 'n/a'; + const absoluteBytes = Math.abs(bytes); + const sign = bytes < 0 ? '-' : ''; + if (absoluteBytes < 1024 * 1024) { + return `${sign}${(absoluteBytes / 1024).toFixed(1)} KiB`; + } + return `${sign}${(absoluteBytes / (1024 * 1024)).toFixed(1)} MiB`; +} + +function formatPercent(value) { + const sign = value > 0 ? '+' : ''; + return `${sign}${value.toFixed(1)}%`; +} + +function parseArgs(argv) { + let repo = 'openclaw/openclaw'; + let k; + let threshold; + let candidateK; + let childBackend = null; + let backend = 'both'; + let maxOldSpaceSizeMb; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token) continue; + if (token === '--repo') { + repo = argv[index + 1] ?? repo; + index += 1; + continue; + } + if (token === '--k') { + k = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--threshold') { + threshold = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--candidate-k') { + candidateK = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--child-backend') { + childBackend = argv[index + 1] ?? null; + index += 1; + continue; + } + if (token === '--backend') { + backend = argv[index + 1] ?? backend; + index += 1; + continue; + } + if (token === '--max-old-space-size') { + maxOldSpaceSizeMb = Number(argv[index + 1]); + index += 1; + continue; + } + if (!token.startsWith('--')) { + repo = token; + } + } + + const [owner, name] = repo.split('/'); + if (!owner || !name) { + throw new Error(`Expected owner/repo, received: ${repo}`); + } + + return { + owner, + repo: name, + fullName: `${owner}/${name}`, + k: Number.isFinite(k) ? k : undefined, + threshold: Number.isFinite(threshold) ? threshold : undefined, + candidateK: Number.isFinite(candidateK) ? candidateK : undefined, + childBackend, + backend, + maxOldSpaceSizeMb: Number.isFinite(maxOldSpaceSizeMb) ? maxOldSpaceSizeMb : undefined, + }; +} + +function getRepoStats(service, fullName) { + const repoRow = service.db + .prepare('select id, full_name from repositories where full_name = ?') + .get(fullName); + if (!repoRow) { + throw new Error(`Repository not found in local DB: ${fullName}`); + } + + const openThreadCount = service.db + .prepare( + `select count(*) as count + from threads + where repo_id = ? + and state = 'open' + and closed_at_local is null`, + ) + .get(repoRow.id).count; + + const embeddingCounts = service.db + .prepare( + `select e.source_kind as sourceKind, count(*) as count + from document_embeddings e + join threads t on t.id = e.thread_id + where t.repo_id = ? + and t.state = 'open' + and t.closed_at_local is null + and e.model = ? + group by e.source_kind + order by e.source_kind asc`, + ) + .all(repoRow.id, service.config.embedModel); + + return { + repoId: repoRow.id, + openThreadCount, + embeddingCounts, + }; +} + +function buildReportLines(label, result) { + return [ + `### ${label}`, + '', + `- Cluster-only duration: ${formatDurationMs(result.durationMs)}`, + `- Total duration: ${formatDurationMs(result.totalDurationMs)}`, + `- Load stage: ${formatDurationMs(result.loadMs)}`, + `- Temp DB setup: ${formatDurationMs(result.setupMs)}`, + `- Exact edge-build: ${formatDurationMs(result.edgeBuildMs)}`, + `- Vector index-build: ${formatDurationMs(result.indexBuildMs)}`, + `- Vector query: ${formatDurationMs(result.queryMs)}`, + `- Cluster assembly: ${formatDurationMs(result.clusterBuildMs)}`, + `- Edges: ${result.edges}`, + `- Clusters: ${result.clusters}`, + `- Threads: ${result.threads}`, + `- Source kinds: ${result.sourceKinds}`, + `- Candidate K: ${result.candidateK}`, + `- Peak RSS: ${formatBytes(result.memory.peakRssBytes)}`, + `- Peak heap used: ${formatBytes(result.memory.peakHeapUsedBytes)}`, + '', + ]; +} + +function buildDeltaLines(exactResult, vectorliteResult) { + const clusterDeltaMs = vectorliteResult.durationMs - exactResult.durationMs; + const clusterDeltaPercent = exactResult.durationMs > 0 ? (clusterDeltaMs / exactResult.durationMs) * 100 : 0; + const totalDeltaMs = vectorliteResult.totalDurationMs - exactResult.totalDurationMs; + const totalDeltaPercent = exactResult.totalDurationMs > 0 ? (totalDeltaMs / exactResult.totalDurationMs) * 100 : 0; + const peakRssDelta = vectorliteResult.memory.peakRssBytes - exactResult.memory.peakRssBytes; + const peakHeapDelta = vectorliteResult.memory.peakHeapUsedBytes - exactResult.memory.peakHeapUsedBytes; + + return [ + '### Delta', + '', + `- Cluster-only delta vs exact: ${formatDurationMs(clusterDeltaMs)} (${formatPercent(clusterDeltaPercent)})`, + `- Total duration delta vs exact: ${formatDurationMs(totalDeltaMs)} (${formatPercent(totalDeltaPercent)})`, + `- Peak RSS delta vs exact: ${formatBytes(peakRssDelta)}`, + `- Peak heap used delta vs exact: ${formatBytes(peakHeapDelta)}`, + '', + ]; +} + +async function runChild(args) { + const service = new GHCrawlService(); + try { + const result = service.clusterExperiment({ + owner: args.owner, + repo: args.repo, + backend: args.childBackend, + k: args.k, + minScore: args.threshold, + candidateK: args.candidateK, + onProgress: (message) => process.stdout.write(`${message}\n`), + }); + process.stdout.write(`__GHCRAWL_RESULT__${JSON.stringify(result)}\n`); + } finally { + service.close(); + } +} + +async function runBackend(backend, args) { + return await new Promise((resolve, reject) => { + const childArgs = ['--expose-gc']; + if (args.maxOldSpaceSizeMb !== undefined) { + childArgs.push(`--max-old-space-size=${args.maxOldSpaceSizeMb}`); + } + childArgs.push( + path.join(repoRoot, 'scripts', 'cluster-perf-real-compare.mjs'), + `${args.fullName}`, + '--child-backend', + backend, + ); + if (args.k !== undefined) { + childArgs.push('--k', String(args.k)); + } + if (args.threshold !== undefined) { + childArgs.push('--threshold', String(args.threshold)); + } + if (args.candidateK !== undefined) { + childArgs.push('--candidate-k', String(args.candidateK)); + } + + const child = spawn(process.execPath, childArgs, { + cwd: repoRoot, + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let result = null; + const pipeStream = (stream, label) => { + const rl = readline.createInterface({ input: stream }); + rl.on('line', (line) => { + if (label === 'stdout' && line.startsWith('__GHCRAWL_RESULT__')) { + result = JSON.parse(line.slice('__GHCRAWL_RESULT__'.length)); + return; + } + process.stdout.write(`[${backend}] ${line}\n`); + }); + }; + + pipeStream(child.stdout, 'stdout'); + pipeStream(child.stderr, 'stderr'); + + child.on('error', reject); + child.on('close', (code, signal) => { + if (code !== 0) { + const detail = signal ? `signal ${signal}` : `code ${code}`; + reject(new Error(`${backend} benchmark exited with ${detail}`)); + return; + } + if (!result) { + reject(new Error(`${backend} benchmark did not emit a result payload`)); + return; + } + resolve(result); + }); + }); +} + +async function runParent(args) { + const service = new GHCrawlService(); + let stats; + let dbPath; + let embedModel; + try { + stats = getRepoStats(service, args.fullName); + dbPath = service.config.dbPath; + embedModel = service.config.embedModel; + } finally { + service.close(); + } + + const lines = [ + '## Real Cluster Perf Comparison', + '', + `- Repo: ${args.fullName}`, + `- Config DB: ${dbPath}`, + `- Embed model: ${embedModel}`, + `- Open threads: ${stats.openThreadCount}`, + `- Embedding counts: ${stats.embeddingCounts.map((row) => `${row.sourceKind}=${row.count}`).join(', ') || 'none'}`, + `- Parameters: k=${args.k ?? 'default'} threshold=${args.threshold ?? 'default'} candidateK=${args.candidateK ?? 'default'}`, + `- Requested backend(s): ${args.backend}`, + `- Child max old space size: ${args.maxOldSpaceSizeMb ?? 'default'}`, + '', + ]; + + let exactResult = null; + let vectorliteResult = null; + + if (args.backend === 'both' || args.backend === 'exact') { + process.stdout.write(`[exact] starting real-db cluster experiment for ${args.fullName}\n`); + exactResult = await runBackend('exact', args); + lines.push(...buildReportLines('Exact', exactResult)); + if (args.backend === 'both') { + process.stdout.write(`\n${lines.join('\n')}\n`); + } + } + + if (args.backend === 'both' || args.backend === 'vectorlite') { + process.stdout.write(`[vectorlite] starting real-db cluster experiment for ${args.fullName}\n`); + vectorliteResult = await runBackend('vectorlite', args); + lines.push(...buildReportLines('Vectorlite', vectorliteResult)); + if (exactResult) { + lines.push(...buildDeltaLines(exactResult, vectorliteResult)); + } + } + + process.stdout.write(`\n${lines.join('\n')}`); +} + +const args = parseArgs(process.argv.slice(2)); +if (args.childBackend === 'exact' || args.childBackend === 'vectorlite') { + await runChild(args); +} else { + await runParent(args); +} From 0a26e31bd39414c1ae5e14fde8fc7a624032f287 Mon Sep 17 00:00:00 2001 From: huntharo Date: Sat, 28 Mar 2026 22:36:28 -0400 Subject: [PATCH 13/34] fix: stream vectorlite cluster experiment inputs --- packages/api-core/src/service.ts | 136 +++++++++++++++++++------------ 1 file changed, 84 insertions(+), 52 deletions(-) diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index 9827c80..f8362a1 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -117,12 +117,6 @@ type StoredEmbeddingRow = ThreadRow & { embedding_json: string; }; -type ParsedStoredEmbeddingRow = Omit & { - embedding: number[]; - normalizedEmbedding: number[]; - embeddingNorm: number; -}; - type ClusterExperimentMemoryStats = { rssBeforeBytes: number; rssAfterBytes: number; @@ -1231,17 +1225,6 @@ export class GHCrawlService { recordMemory(); } } else { - const loadStartedAt = Date.now(); - const rows = this.loadParsedStoredEmbeddings(repository.id); - const bySource = new Map(); - for (const row of rows) { - const list = bySource.get(row.source_kind) ?? []; - list.push(row); - bySource.set(row.source_kind, list); - } - loadMs += Date.now() - loadStartedAt; - recordMemory(); - const setupStartedAt = Date.now(); tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghcrawl-vectorlite-')); tempDbPath = path.join(tempDir, 'cluster-experiment.db'); @@ -1255,40 +1238,50 @@ export class GHCrawlService { recordMemory(); for (const sourceKind of sourceKinds) { - const sourceRows = bySource.get(sourceKind) ?? []; - if (sourceRows.length === 0) { + const sourceRowCount = this.countEmbeddingsForSourceKind(repository.id, sourceKind); + if (sourceRowCount === 0) { + continue; + } + + const firstRow = this.loadNormalizedEmbeddingForSourceKindHead(repository.id, sourceKind); + if (!firstRow) { continue; } const tableName = `vector_${sourceKind}`; - const dimension = sourceRows[0]?.normalizedEmbedding.length ?? 0; - const safeCandidateK = Math.min(candidateK, Math.max(1, sourceRows.length - 1)); + const dimension = firstRow.normalizedEmbedding.length; + const safeCandidateK = Math.min(candidateK, Math.max(1, sourceRowCount - 1)); params.onProgress?.( - `[cluster-experiment] building ${sourceKind} HNSW index with ${sourceRows.length} vector(s)`, + `[cluster-experiment] building ${sourceKind} HNSW index with ${sourceRowCount} vector(s)`, ); const indexStartedAt = Date.now(); tempDb.exec( - `create virtual table ${tableName} using vectorlite(vec float32[${dimension}], hnsw(max_elements=${sourceRows.length}));`, + `create virtual table ${tableName} using vectorlite(vec float32[${dimension}], hnsw(max_elements=${sourceRowCount}));`, ); const insert = tempDb.prepare(`insert into ${tableName}(rowid, vec) values (?, ?)`); tempDb.transaction(() => { - for (const row of sourceRows) { + const loadStartedAt = Date.now(); + for (const row of this.iterateNormalizedEmbeddingsForSourceKind(repository.id, sourceKind)) { insert.run(row.id, this.normalizedEmbeddingBuffer(row.normalizedEmbedding)); } + loadMs += Date.now() - loadStartedAt; })(); indexBuildMs += Date.now() - indexStartedAt; recordMemory(); - const rowsById = new Map(sourceRows.map((row) => [row.id, row])); const queryStartedAt = Date.now(); const query = tempDb.prepare( - `select rowid from ${tableName} where knn_search(vec, knn_param(?, ${safeCandidateK + 1}))`, + `select rowid, distance from ${tableName} where knn_search(vec, knn_param(?, ${safeCandidateK + 1}))`, ); let processed = 0; let lastProgressAt = Date.now(); - for (const row of sourceRows) { - const candidates = query.all(this.normalizedEmbeddingBuffer(row.normalizedEmbedding)) as Array<{ rowid: number }>; + const queryLoadStartedAt = Date.now(); + for (const row of this.iterateNormalizedEmbeddingsForSourceKind(repository.id, sourceKind)) { + const candidates = query.all(this.normalizedEmbeddingBuffer(row.normalizedEmbedding)) as Array<{ + rowid: number; + distance: number; + }>; const ranked = rankNearestNeighborsByScore(candidates, { limit: k, minScore, @@ -1296,19 +1289,13 @@ export class GHCrawlService { if (candidate.rowid === row.id) { return -1; } - const candidateRow = rowsById.get(candidate.rowid); - if (!candidateRow) { - return -1; - } - return dotProduct(row.normalizedEmbedding, candidateRow.normalizedEmbedding); + return this.normalizedDistanceToScore(candidate.distance); }, }); let addedThisRow = 0; for (const candidate of ranked) { - const candidateRow = rowsById.get(candidate.item.rowid); - if (!candidateRow) continue; const score = candidate.score; - const key = this.edgeKey(row.id, candidateRow.id); + const key = this.edgeKey(row.id, candidate.item.rowid); const existing = aggregated.get(key); if (existing) { existing.score = Math.max(existing.score, score); @@ -1316,8 +1303,8 @@ export class GHCrawlService { continue; } aggregated.set(key, { - leftThreadId: Math.min(row.id, candidateRow.id), - rightThreadId: Math.max(row.id, candidateRow.id), + leftThreadId: Math.min(row.id, candidate.item.rowid), + rightThreadId: Math.max(row.id, candidate.item.rowid), score, sourceKinds: new Set([sourceKind]), }); @@ -1328,11 +1315,12 @@ export class GHCrawlService { if (params.onProgress && now - lastProgressAt >= CLUSTER_PROGRESS_INTERVAL_MS) { recordMemory(); params.onProgress( - `[cluster-experiment] querying ${sourceKind} index ${processed}/${sourceRows.length} current_edges=${aggregated.size} added_this_step=${addedThisRow}`, + `[cluster-experiment] querying ${sourceKind} index ${processed}/${sourceRowCount} current_edges=${aggregated.size} added_this_step=${addedThisRow}`, ); lastProgressAt = now; } } + loadMs += Date.now() - queryLoadStartedAt; queryMs += Date.now() - queryStartedAt; tempDb.exec(`drop table ${tableName}`); recordMemory(); @@ -3086,19 +3074,6 @@ export class GHCrawlService { .all(repoId, this.config.embedModel) as StoredEmbeddingRow[]; } - private loadParsedStoredEmbeddings(repoId: number): ParsedStoredEmbeddingRow[] { - return this.loadStoredEmbeddings(repoId).map((row) => { - const embedding = JSON.parse(row.embedding_json) as number[]; - const normalized = normalizeEmbedding(embedding); - return { - ...row, - embedding, - normalizedEmbedding: normalized.normalized, - embeddingNorm: normalized.norm, - }; - }); - } - private loadStoredEmbeddingsForThreadNumber(repoId: number, threadNumber: number): StoredEmbeddingRow[] { return this.db .prepare( @@ -3131,6 +3106,59 @@ export class GHCrawlService { .iterate(repoId, this.config.embedModel) as IterableIterator; } + private loadNormalizedEmbeddingForSourceKindHead( + repoId: number, + sourceKind: EmbeddingSourceKind, + ): { id: number; normalizedEmbedding: number[] } | null { + const row = this.db + .prepare( + `select t.id, e.embedding_json + from threads t + join document_embeddings e on e.thread_id = t.id + where t.repo_id = ? + and t.state = 'open' + and t.closed_at_local is null + and e.model = ? + and e.source_kind = ? + order by t.number asc + limit 1`, + ) + .get(repoId, this.config.embedModel, sourceKind) as { id: number; embedding_json: string } | undefined; + if (!row) { + return null; + } + return { + id: row.id, + normalizedEmbedding: normalizeEmbedding(JSON.parse(row.embedding_json) as number[]).normalized, + }; + } + + private *iterateNormalizedEmbeddingsForSourceKind( + repoId: number, + sourceKind: EmbeddingSourceKind, + ): IterableIterator<{ id: number; normalizedEmbedding: number[] }> { + const rows = this.db + .prepare( + `select t.id, e.embedding_json + from threads t + join document_embeddings e on e.thread_id = t.id + where t.repo_id = ? + and t.state = 'open' + and t.closed_at_local is null + and e.model = ? + and e.source_kind = ? + order by t.number asc`, + ) + .iterate(repoId, this.config.embedModel, sourceKind) as IterableIterator<{ id: number; embedding_json: string }>; + + for (const row of rows) { + yield { + id: row.id, + normalizedEmbedding: normalizeEmbedding(JSON.parse(row.embedding_json) as number[]).normalized, + }; + } + } + private loadNormalizedEmbeddingsForSourceKind( repoId: number, sourceKind: EmbeddingSourceKind, @@ -3159,6 +3187,10 @@ export class GHCrawlService { return Buffer.from(Float32Array.from(values).buffer); } + private normalizedDistanceToScore(distance: number): number { + return 1 - distance / 2; + } + private loadClusterableThreadMeta(repoId: number): { items: Array<{ id: number; number: number; title: string }>; sourceKinds: EmbeddingSourceKind[]; From 54c7e5077d1f837d9ae8a4810c1d67a698e3e35a Mon Sep 17 00:00:00 2001 From: huntharo Date: Sat, 28 Mar 2026 23:05:29 -0400 Subject: [PATCH 14/34] feat: compare cluster population distributions --- package.json | 1 + packages/api-core/src/service.ts | 39 ++++ scripts/cluster-population-compare.mjs | 280 +++++++++++++++++++++++++ 3 files changed, 320 insertions(+) create mode 100644 scripts/cluster-population-compare.mjs diff --git a/package.json b/package.json index 7856296..861000e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "test:cluster-perf": "pnpm --filter @ghcrawl/api-core test:cluster-perf", "perf:cluster:large": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-perf-large-compare.mjs", "perf:cluster:real": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-perf-real-compare.mjs", + "perf:cluster:population": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-population-compare.mjs", "pack:smoke": "node ./scripts/pack-smoke.mjs", "release:metadata": "node ./scripts/release-metadata.mjs", "release:apply-version": "node ./scripts/apply-release-version.mjs", diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index f8362a1..13d4583 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -126,6 +126,18 @@ type ClusterExperimentMemoryStats = { peakHeapUsedBytes: number; }; +type ClusterExperimentSizeBucket = { + size: number; + count: number; +}; + +type ClusterExperimentClusterSizeStats = { + soloClusters: number; + maxClusterSize: number; + topClusterSizes: number[]; + histogram: ClusterExperimentSizeBucket[]; +}; + type ClusterExperimentResult = { backend: 'exact' | 'vectorlite'; repository: RepositoryDto; @@ -145,6 +157,7 @@ type ClusterExperimentResult = { clusterBuildMs: number; candidateK: number; memory: ClusterExperimentMemoryStats; + clusterSizes: ClusterExperimentClusterSizeStats; }; type EmbeddingWorkset = { rows: Array<{ @@ -1371,6 +1384,7 @@ export class GHCrawlService { heapUsedAfterBytes: memoryAfter.heapUsed, peakHeapUsedBytes, }, + clusterSizes: this.summarizeClusterSizes(clusters), }; } finally { tempDb?.close(); @@ -3573,6 +3587,31 @@ export class GHCrawlService { this.db.prepare('delete from cluster_runs where repo_id = ? and id <> ?').run(repoId, keepRunId); } + private summarizeClusterSizes( + clusters: Array<{ representativeThreadId: number; members: number[] }>, + ): ClusterExperimentClusterSizeStats { + const histogramCounts = new Map(); + const topClusterSizes = clusters.map((cluster) => cluster.members.length).sort((left, right) => right - left); + let soloClusters = 0; + + for (const cluster of clusters) { + const size = cluster.members.length; + histogramCounts.set(size, (histogramCounts.get(size) ?? 0) + 1); + if (size === 1) { + soloClusters += 1; + } + } + + return { + soloClusters, + maxClusterSize: topClusterSizes[0] ?? 0, + topClusterSizes: topClusterSizes.slice(0, 50), + histogram: Array.from(histogramCounts.entries()) + .map(([size, count]) => ({ size, count })) + .sort((left, right) => left.size - right.size), + }; + } + private upsertSummary(threadId: number, contentHash: string, summaryKind: string, summaryText: string): void { this.db .prepare( diff --git a/scripts/cluster-population-compare.mjs b/scripts/cluster-population-compare.mjs new file mode 100644 index 0000000..82435c4 --- /dev/null +++ b/scripts/cluster-population-compare.mjs @@ -0,0 +1,280 @@ +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import readline from 'node:readline'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const serviceModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'service.js'); + +const { GHCrawlService } = await import(serviceModulePath); + +function parseArgs(argv) { + let repo = 'openclaw/openclaw'; + let k; + let threshold; + let candidateK; + let childBackend = null; + let top = 20; + let maxSize = 20; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token) continue; + if (token === '--repo') { + repo = argv[index + 1] ?? repo; + index += 1; + continue; + } + if (token === '--k') { + k = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--threshold') { + threshold = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--candidate-k') { + candidateK = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--child-backend') { + childBackend = argv[index + 1] ?? null; + index += 1; + continue; + } + if (token === '--top') { + top = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--max-size') { + maxSize = Number(argv[index + 1]); + index += 1; + continue; + } + if (!token.startsWith('--')) { + repo = token; + } + } + + const [owner, name] = repo.split('/'); + if (!owner || !name) { + throw new Error(`Expected owner/repo, received: ${repo}`); + } + + return { + owner, + repo: name, + fullName: `${owner}/${name}`, + k: Number.isFinite(k) ? k : undefined, + threshold: Number.isFinite(threshold) ? threshold : undefined, + candidateK: Number.isFinite(candidateK) ? candidateK : undefined, + childBackend, + top: Number.isFinite(top) ? Math.max(1, top) : 20, + maxSize: Number.isFinite(maxSize) ? Math.max(1, maxSize) : 20, + }; +} + +function formatPercent(value) { + return `${(value * 100).toFixed(1)}%`; +} + +function countThreadsRepresented(histogram) { + return histogram.reduce((sum, bucket) => sum + bucket.size * bucket.count, 0); +} + +function histogramToMap(histogram) { + return new Map(histogram.map((bucket) => [bucket.size, bucket.count])); +} + +function formatDelta(value) { + return value > 0 ? `+${value}` : String(value); +} + +function repeat(character, count) { + return count > 0 ? character.repeat(count) : ''; +} + +function buildBar(count, maxCount, width) { + if (maxCount <= 0) return ''; + const scaled = Math.round((count / maxCount) * width); + return repeat('#', scaled); +} + +async function runChild(args) { + const service = new GHCrawlService(); + try { + const result = service.clusterExperiment({ + owner: args.owner, + repo: args.repo, + backend: args.childBackend, + k: args.k, + minScore: args.threshold, + candidateK: args.candidateK, + onProgress: (message) => process.stdout.write(`${message}\n`), + }); + process.stdout.write(`__GHCRAWL_RESULT__${JSON.stringify(result)}\n`); + } finally { + service.close(); + } +} + +async function runBackend(backend, args) { + return await new Promise((resolve, reject) => { + const childArgs = [ + '--expose-gc', + path.join(repoRoot, 'scripts', 'cluster-population-compare.mjs'), + args.fullName, + '--child-backend', + backend, + '--top', + String(args.top), + '--max-size', + String(args.maxSize), + ]; + if (args.k !== undefined) { + childArgs.push('--k', String(args.k)); + } + if (args.threshold !== undefined) { + childArgs.push('--threshold', String(args.threshold)); + } + if (args.candidateK !== undefined) { + childArgs.push('--candidate-k', String(args.candidateK)); + } + + const child = spawn(process.execPath, childArgs, { + cwd: repoRoot, + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let result = null; + const pipeStream = (stream, label) => { + const rl = readline.createInterface({ input: stream }); + rl.on('line', (line) => { + if (label === 'stdout' && line.startsWith('__GHCRAWL_RESULT__')) { + result = JSON.parse(line.slice('__GHCRAWL_RESULT__'.length)); + return; + } + process.stdout.write(`[${backend}] ${line}\n`); + }); + }; + + pipeStream(child.stdout, 'stdout'); + pipeStream(child.stderr, 'stderr'); + + child.on('error', reject); + child.on('close', (code, signal) => { + if (code !== 0) { + const detail = signal ? `signal ${signal}` : `code ${code}`; + reject(new Error(`${backend} comparison exited with ${detail}`)); + return; + } + if (!result) { + reject(new Error(`${backend} comparison did not emit a result payload`)); + return; + } + resolve(result); + }); + }); +} + +function buildSummaryLines(args, exactResult, vectorliteResult) { + const exactHistogram = exactResult.clusterSizes.histogram; + const vectorHistogram = vectorliteResult.clusterSizes.histogram; + const exactThreadsRepresented = countThreadsRepresented(exactHistogram); + const vectorThreadsRepresented = countThreadsRepresented(vectorHistogram); + + return [ + '## Cluster Population Comparison', + '', + `- Repo: ${exactResult.repository.fullName}`, + `- Parameters: k=${args.k ?? 'default'} threshold=${args.threshold ?? 'default'} candidateK=${args.candidateK ?? 'default'}`, + `- Exact clusters: ${exactResult.clusters}`, + `- Vectorlite clusters: ${vectorliteResult.clusters}`, + `- Exact solo clusters: ${exactResult.clusterSizes.soloClusters} (${formatPercent(exactResult.clusterSizes.soloClusters / Math.max(exactResult.clusters, 1))})`, + `- Vectorlite solo clusters: ${vectorliteResult.clusterSizes.soloClusters} (${formatPercent(vectorliteResult.clusterSizes.soloClusters / Math.max(vectorliteResult.clusters, 1))})`, + `- Exact max cluster size: ${exactResult.clusterSizes.maxClusterSize}`, + `- Vectorlite max cluster size: ${vectorliteResult.clusterSizes.maxClusterSize}`, + `- Exact threads represented: ${exactThreadsRepresented}`, + `- Vectorlite threads represented: ${vectorThreadsRepresented}`, + '', + ]; +} + +function buildTopSizesLines(exactResult, vectorliteResult, topCount) { + const exactTop = exactResult.clusterSizes.topClusterSizes.slice(0, topCount); + const vectorTop = vectorliteResult.clusterSizes.topClusterSizes.slice(0, topCount); + const lines = ['## Largest Cluster Sizes', '', 'rank exact vectorlite delta', '---- ----- ---------- -----']; + + for (let index = 0; index < topCount; index += 1) { + const exactSize = exactTop[index] ?? 0; + const vectorSize = vectorTop[index] ?? 0; + lines.push( + `${String(index + 1).padStart(4)} ${String(exactSize).padStart(5)} ${String(vectorSize).padStart(10)} ${formatDelta(vectorSize - exactSize).padStart(5)}`, + ); + } + + lines.push(''); + return lines; +} + +function buildHistogramLines(exactResult, vectorliteResult, maxSize) { + const exactMap = histogramToMap(exactResult.clusterSizes.histogram); + const vectorMap = histogramToMap(vectorliteResult.clusterSizes.histogram); + const exactOverflow = exactResult.clusterSizes.histogram + .filter((bucket) => bucket.size > maxSize) + .reduce((sum, bucket) => sum + bucket.count, 0); + const vectorOverflow = vectorliteResult.clusterSizes.histogram + .filter((bucket) => bucket.size > maxSize) + .reduce((sum, bucket) => sum + bucket.count, 0); + + let maxCount = 0; + for (let size = 1; size <= maxSize; size += 1) { + maxCount = Math.max(maxCount, exactMap.get(size) ?? 0, vectorMap.get(size) ?? 0); + } + maxCount = Math.max(maxCount, exactOverflow, vectorOverflow); + + const lines = ['## Histogram By Cluster Size', '', 'size exact vectorlite delta bars', '---- ----- ---------- ----- ----']; + for (let size = 1; size <= maxSize; size += 1) { + const exactCount = exactMap.get(size) ?? 0; + const vectorCount = vectorMap.get(size) ?? 0; + const exactBar = buildBar(exactCount, maxCount, 12); + const vectorBar = buildBar(vectorCount, maxCount, 12); + lines.push( + `${String(size).padStart(4)} ${String(exactCount).padStart(5)} ${String(vectorCount).padStart(10)} ${formatDelta(vectorCount - exactCount).padStart(5)} E:${exactBar.padEnd(12)} V:${vectorBar.padEnd(12)}`, + ); + } + + lines.push( + `${`${maxSize}+`.padStart(4)} ${String(exactOverflow).padStart(5)} ${String(vectorOverflow).padStart(10)} ${formatDelta(vectorOverflow - exactOverflow).padStart(5)} E:${buildBar(exactOverflow, maxCount, 12).padEnd(12)} V:${buildBar(vectorOverflow, maxCount, 12).padEnd(12)}`, + ); + lines.push(''); + return lines; +} + +async function runParent(args) { + process.stdout.write(`[exact] starting cluster population comparison for ${args.fullName}\n`); + const exactResult = await runBackend('exact', args); + + process.stdout.write(`[vectorlite] starting cluster population comparison for ${args.fullName}\n`); + const vectorliteResult = await runBackend('vectorlite', args); + + const lines = [ + ...buildSummaryLines(args, exactResult, vectorliteResult), + ...buildTopSizesLines(exactResult, vectorliteResult, args.top), + ...buildHistogramLines(exactResult, vectorliteResult, args.maxSize), + ]; + + process.stdout.write(`\n${lines.join('\n')}`); +} + +const args = parseArgs(process.argv.slice(2)); +if (args.childBackend === 'exact' || args.childBackend === 'vectorlite') { + await runChild(args); +} else { + await runParent(args); +} From b91a52238be24c119b10d1b72dc30ee1164d52ad Mon Sep 17 00:00:00 2001 From: huntharo Date: Sat, 28 Mar 2026 23:31:29 -0400 Subject: [PATCH 15/34] feat: inspect cluster topology differences --- package.json | 1 + packages/api-core/src/service.ts | 13 + scripts/cluster-topology-compare.mjs | 379 +++++++++++++++++++++++++++ 3 files changed, 393 insertions(+) create mode 100644 scripts/cluster-topology-compare.mjs diff --git a/package.json b/package.json index 861000e..65ad872 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "perf:cluster:large": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-perf-large-compare.mjs", "perf:cluster:real": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-perf-real-compare.mjs", "perf:cluster:population": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-population-compare.mjs", + "perf:cluster:topology": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-topology-compare.mjs", "pack:smoke": "node ./scripts/pack-smoke.mjs", "release:metadata": "node ./scripts/release-metadata.mjs", "release:apply-version": "node ./scripts/apply-release-version.mjs", diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index 13d4583..be7de7d 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -138,6 +138,11 @@ type ClusterExperimentClusterSizeStats = { histogram: ClusterExperimentSizeBucket[]; }; +type ClusterExperimentCluster = { + representativeThreadId: number; + memberThreadIds: number[]; +}; + type ClusterExperimentResult = { backend: 'exact' | 'vectorlite'; repository: RepositoryDto; @@ -158,6 +163,7 @@ type ClusterExperimentResult = { candidateK: number; memory: ClusterExperimentMemoryStats; clusterSizes: ClusterExperimentClusterSizeStats; + clustersDetail: ClusterExperimentCluster[] | null; }; type EmbeddingWorkset = { rows: Array<{ @@ -1174,6 +1180,7 @@ export class GHCrawlService { minScore?: number; k?: number; candidateK?: number; + includeClusters?: boolean; onProgress?: (message: string) => void; }): ClusterExperimentResult { const backend = params.backend ?? 'vectorlite'; @@ -1385,6 +1392,12 @@ export class GHCrawlService { peakHeapUsedBytes, }, clusterSizes: this.summarizeClusterSizes(clusters), + clustersDetail: params.includeClusters + ? clusters.map((cluster) => ({ + representativeThreadId: cluster.representativeThreadId, + memberThreadIds: [...cluster.members], + })) + : null, }; } finally { tempDb?.close(); diff --git a/scripts/cluster-topology-compare.mjs b/scripts/cluster-topology-compare.mjs new file mode 100644 index 0000000..3ce8ca8 --- /dev/null +++ b/scripts/cluster-topology-compare.mjs @@ -0,0 +1,379 @@ +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import readline from 'node:readline'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const serviceModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'service.js'); + +const { GHCrawlService } = await import(serviceModulePath); + +function parseArgs(argv) { + let repo = 'openclaw/openclaw'; + let k; + let threshold; + let candidateK; + let childBackend = null; + let top = 5; + let sampleMembers = 12; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token) continue; + if (token === '--repo') { + repo = argv[index + 1] ?? repo; + index += 1; + continue; + } + if (token === '--k') { + k = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--threshold') { + threshold = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--candidate-k') { + candidateK = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--child-backend') { + childBackend = argv[index + 1] ?? null; + index += 1; + continue; + } + if (token === '--top') { + top = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--sample-members') { + sampleMembers = Number(argv[index + 1]); + index += 1; + continue; + } + if (!token.startsWith('--')) { + repo = token; + } + } + + const [owner, name] = repo.split('/'); + if (!owner || !name) { + throw new Error(`Expected owner/repo, received: ${repo}`); + } + + return { + owner, + repo: name, + fullName: `${owner}/${name}`, + k: Number.isFinite(k) ? k : undefined, + threshold: Number.isFinite(threshold) ? threshold : undefined, + candidateK: Number.isFinite(candidateK) ? candidateK : undefined, + childBackend, + top: Number.isFinite(top) ? Math.max(1, top) : 5, + sampleMembers: Number.isFinite(sampleMembers) ? Math.max(1, sampleMembers) : 12, + }; +} + +async function runChild(args) { + const service = new GHCrawlService(); + try { + const result = service.clusterExperiment({ + owner: args.owner, + repo: args.repo, + backend: args.childBackend, + k: args.k, + minScore: args.threshold, + candidateK: args.candidateK, + includeClusters: true, + onProgress: (message) => process.stdout.write(`${message}\n`), + }); + process.stdout.write(`__GHCRAWL_RESULT__${JSON.stringify(result)}\n`); + } finally { + service.close(); + } +} + +async function runBackend(backend, args) { + return await new Promise((resolve, reject) => { + const childArgs = [ + '--expose-gc', + path.join(repoRoot, 'scripts', 'cluster-topology-compare.mjs'), + args.fullName, + '--child-backend', + backend, + '--top', + String(args.top), + '--sample-members', + String(args.sampleMembers), + ]; + if (args.k !== undefined) { + childArgs.push('--k', String(args.k)); + } + if (args.threshold !== undefined) { + childArgs.push('--threshold', String(args.threshold)); + } + if (args.candidateK !== undefined) { + childArgs.push('--candidate-k', String(args.candidateK)); + } + + const child = spawn(process.execPath, childArgs, { + cwd: repoRoot, + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let result = null; + const pipeStream = (stream, label) => { + const rl = readline.createInterface({ input: stream }); + rl.on('line', (line) => { + if (label === 'stdout' && line.startsWith('__GHCRAWL_RESULT__')) { + result = JSON.parse(line.slice('__GHCRAWL_RESULT__'.length)); + return; + } + process.stdout.write(`[${backend}] ${line}\n`); + }); + }; + + pipeStream(child.stdout, 'stdout'); + pipeStream(child.stderr, 'stderr'); + + child.on('error', reject); + child.on('close', (code, signal) => { + if (code !== 0) { + const detail = signal ? `signal ${signal}` : `code ${code}`; + reject(new Error(`${backend} topology comparison exited with ${detail}`)); + return; + } + if (!result) { + reject(new Error(`${backend} topology comparison did not emit a result payload`)); + return; + } + resolve(result); + }); + }); +} + +function sortClusters(clusters) { + return [...clusters].sort((left, right) => { + const sizeDelta = right.memberThreadIds.length - left.memberThreadIds.length; + if (sizeDelta !== 0) return sizeDelta; + return left.representativeThreadId - right.representativeThreadId; + }); +} + +function buildClusterIndex(clusters) { + return clusters.map((cluster, index) => ({ + rank: index + 1, + representativeThreadId: cluster.representativeThreadId, + memberThreadIds: cluster.memberThreadIds, + memberSet: new Set(cluster.memberThreadIds), + size: cluster.memberThreadIds.length, + })); +} + +function findContributors(targetCluster, sourceClusters) { + const contributors = []; + for (const sourceCluster of sourceClusters) { + let overlap = 0; + for (const threadId of targetCluster.memberThreadIds) { + if (sourceCluster.memberSet.has(threadId)) { + overlap += 1; + } + } + if (overlap > 0) { + contributors.push({ + rank: sourceCluster.rank, + representativeThreadId: sourceCluster.representativeThreadId, + size: sourceCluster.size, + overlap, + }); + } + } + contributors.sort((left, right) => right.overlap - left.overlap || left.rank - right.rank); + return contributors; +} + +function collectSampleIds(vectorTop, exactTop, matches, sampleMembers) { + const ids = new Set(); + for (const cluster of [...vectorTop, ...exactTop]) { + ids.add(cluster.representativeThreadId); + } + for (const match of matches) { + if (!match.bestContributor) { + continue; + } + ids.add(match.bestContributor.representativeThreadId); + for (const contributor of match.contributors.slice(0, 5)) { + ids.add(contributor.representativeThreadId); + } + for (const threadId of match.vectorOnly.slice(0, sampleMembers)) { + ids.add(threadId); + } + for (const threadId of match.bestExactOnly.slice(0, sampleMembers)) { + ids.add(threadId); + } + } + return [...ids]; +} + +function fetchThreadMeta(ids) { + if (ids.length === 0) { + return new Map(); + } + + const service = new GHCrawlService(); + try { + const placeholders = ids.map(() => '?').join(', '); + const rows = service.db + .prepare( + `select id, number, kind, title + from threads + where id in (${placeholders})`, + ) + .all(...ids); + return new Map(rows.map((row) => [row.id, row])); + } finally { + service.close(); + } +} + +function describeThread(threadId, metaById) { + const meta = metaById.get(threadId); + if (!meta) { + return `thread:${threadId}`; + } + const kind = meta.kind === 'pull_request' ? 'PR' : 'Issue'; + return `${kind} #${meta.number} ${meta.title}`; +} + +function formatContributor(contributor, targetSize) { + const coverage = ((contributor.overlap / Math.max(targetSize, 1)) * 100).toFixed(1); + return `exact #${contributor.rank} size=${contributor.size} overlap=${contributor.overlap} (${coverage}% of vector cluster)`; +} + +function buildSummaryTable(exactTop, vectorTop, matches) { + const lines = ['## Top Cluster Size Comparison', '', 'rank exact vectorlite best exact overlap', '---- ----- ---------- ------------------']; + for (let index = 0; index < Math.max(exactTop.length, vectorTop.length); index += 1) { + const exactSize = exactTop[index]?.size ?? 0; + const vectorSize = vectorTop[index]?.size ?? 0; + const overlap = matches[index]?.bestContributor?.overlap ?? 0; + lines.push( + `${String(index + 1).padStart(4)} ${String(exactSize).padStart(5)} ${String(vectorSize).padStart(10)} ${String(overlap).padStart(18)}`, + ); + } + lines.push(''); + return lines; +} + +function buildDetailLines(vectorTop, matches, metaById, sampleMembers) { + const lines = ['## Largest Vectorlite Clusters Vs Exact', '']; + + for (let index = 0; index < vectorTop.length; index += 1) { + const vectorCluster = vectorTop[index]; + const match = matches[index]; + const representative = describeThread(vectorCluster.representativeThreadId, metaById); + + lines.push( + `### Vectorlite #${index + 1} size=${vectorCluster.size} representative=${representative}`, + ); + + if (!match.bestContributor) { + lines.push('- No overlapping exact cluster found.'); + lines.push(''); + continue; + } + + const contributorSummary = match.contributors + .slice(0, 5) + .map((contributor) => formatContributor(contributor, vectorCluster.size)) + .join('; '); + lines.push(`- Top exact contributors: ${contributorSummary}`); + + const bestRepresentative = describeThread(match.bestContributor.representativeThreadId, metaById); + lines.push(`- Best exact representative: ${bestRepresentative}`); + lines.push( + `- Members only in vectorlite vs best exact: ${match.vectorOnly.length}; members only in best exact vs vectorlite: ${match.bestExactOnly.length}`, + ); + + if (match.vectorOnly.length > 0) { + lines.push( + `- Sample vectorlite-only members: ${match.vectorOnly + .slice(0, sampleMembers) + .map((threadId) => describeThread(threadId, metaById)) + .join(' | ')}`, + ); + } + + if (match.bestExactOnly.length > 0) { + lines.push( + `- Sample exact-only members: ${match.bestExactOnly + .slice(0, sampleMembers) + .map((threadId) => describeThread(threadId, metaById)) + .join(' | ')}`, + ); + } + + lines.push(''); + } + + return lines; +} + +async function runParent(args) { + process.stdout.write(`[exact] starting topology comparison for ${args.fullName}\n`); + const exactResult = await runBackend('exact', args); + + process.stdout.write(`[vectorlite] starting topology comparison for ${args.fullName}\n`); + const vectorliteResult = await runBackend('vectorlite', args); + + const exactClusters = buildClusterIndex(sortClusters(exactResult.clustersDetail ?? [])); + const vectorClusters = buildClusterIndex(sortClusters(vectorliteResult.clustersDetail ?? [])); + const exactTop = exactClusters.slice(0, args.top); + const vectorTop = vectorClusters.slice(0, args.top); + + const matches = vectorTop.map((vectorCluster) => { + const contributors = findContributors(vectorCluster, exactClusters); + const bestContributor = contributors[0] ?? null; + const bestExactSet = bestContributor + ? exactClusters[bestContributor.rank - 1].memberSet + : new Set(); + const vectorOnly = vectorCluster.memberThreadIds.filter((threadId) => !bestExactSet.has(threadId)); + const bestExactOnly = bestContributor + ? exactClusters[bestContributor.rank - 1].memberThreadIds.filter((threadId) => !vectorCluster.memberSet.has(threadId)) + : []; + + return { + contributors, + bestContributor, + vectorOnly, + bestExactOnly, + }; + }); + + const metaById = fetchThreadMeta(collectSampleIds(vectorTop, exactTop, matches, args.sampleMembers)); + + const lines = [ + '## Cluster Topology Comparison', + '', + `- Repo: ${args.fullName}`, + `- Parameters: k=${args.k ?? 'default'} threshold=${args.threshold ?? 'default'} candidateK=${args.candidateK ?? 'default'}`, + `- Exact clusters: ${exactResult.clusters}`, + `- Vectorlite clusters: ${vectorliteResult.clusters}`, + '', + ...buildSummaryTable(exactTop, vectorTop, matches), + ...buildDetailLines(vectorTop, matches, metaById, args.sampleMembers), + ]; + + process.stdout.write(`\n${lines.join('\n')}`); +} + +const args = parseArgs(process.argv.slice(2)); +if (args.childBackend === 'exact' || args.childBackend === 'vectorlite') { + await runChild(args); +} else { + await runParent(args); +} From 82fc6b9901bb1ab14150b7d8917bbeca9ee64457 Mon Sep 17 00:00:00 2001 From: huntharo Date: Sun, 29 Mar 2026 09:16:33 -0400 Subject: [PATCH 16/34] feat: refine oversized vector clusters --- package.json | 1 + packages/api-core/src/service.ts | 10 +- scripts/cluster-refine-component.mjs | 247 +++++++++++++++++++++++++++ scripts/cluster-topology-compare.mjs | 12 ++ 4 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 scripts/cluster-refine-component.mjs diff --git a/package.json b/package.json index 65ad872..599ad15 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "perf:cluster:real": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-perf-real-compare.mjs", "perf:cluster:population": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-population-compare.mjs", "perf:cluster:topology": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-topology-compare.mjs", + "perf:cluster:refine": "pnpm --filter @ghcrawl/api-core build && node ./scripts/cluster-refine-component.mjs", "pack:smoke": "node ./scripts/pack-smoke.mjs", "release:metadata": "node ./scripts/release-metadata.mjs", "release:apply-version": "node ./scripts/apply-release-version.mjs", diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index be7de7d..4515db5 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -1180,6 +1180,7 @@ export class GHCrawlService { minScore?: number; k?: number; candidateK?: number; + efSearch?: number; includeClusters?: boolean; onProgress?: (message: string) => void; }): ClusterExperimentResult { @@ -1189,6 +1190,7 @@ export class GHCrawlService { const minScore = params.minScore ?? 0.82; const k = params.k ?? 6; const candidateK = Math.max(k, params.candidateK ?? Math.max(k * 16, 64)); + const efSearch = params.efSearch; const startedAt = Date.now(); const memoryBefore = process.memoryUsage(); let peakRssBytes = memoryBefore.rss; @@ -1291,9 +1293,11 @@ export class GHCrawlService { recordMemory(); const queryStartedAt = Date.now(); - const query = tempDb.prepare( - `select rowid, distance from ${tableName} where knn_search(vec, knn_param(?, ${safeCandidateK + 1}))`, - ); + const querySql = + efSearch !== undefined + ? `select rowid, distance from ${tableName} where knn_search(vec, knn_param(?, ${safeCandidateK + 1}, ${efSearch}))` + : `select rowid, distance from ${tableName} where knn_search(vec, knn_param(?, ${safeCandidateK + 1}))`; + const query = tempDb.prepare(querySql); let processed = 0; let lastProgressAt = Date.now(); const queryLoadStartedAt = Date.now(); diff --git a/scripts/cluster-refine-component.mjs b/scripts/cluster-refine-component.mjs new file mode 100644 index 0000000..eb1737a --- /dev/null +++ b/scripts/cluster-refine-component.mjs @@ -0,0 +1,247 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const serviceModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'service.js'); +const buildModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'cluster', 'build.js'); +const exactEdgesModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'cluster', 'exact-edges.js'); + +const { GHCrawlService } = await import(serviceModulePath); +const { buildClusters } = await import(buildModulePath); +const { buildSourceKindEdges } = await import(exactEdgesModulePath); + +function parseArgs(argv) { + let repo = 'openclaw/openclaw'; + let clusterRank = 1; + let backend = 'vectorlite'; + let k; + let threshold; + let candidateK; + let efSearch; + let topSubclusters = 10; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token) continue; + if (token === '--repo') { + repo = argv[index + 1] ?? repo; + index += 1; + continue; + } + if (token === '--cluster-rank') { + clusterRank = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--backend') { + backend = argv[index + 1] ?? backend; + index += 1; + continue; + } + if (token === '--k') { + k = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--threshold') { + threshold = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--candidate-k') { + candidateK = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--ef-search') { + efSearch = Number(argv[index + 1]); + index += 1; + continue; + } + if (token === '--top-subclusters') { + topSubclusters = Number(argv[index + 1]); + index += 1; + continue; + } + if (!token.startsWith('--')) { + repo = token; + } + } + + const [owner, name] = repo.split('/'); + if (!owner || !name) { + throw new Error(`Expected owner/repo, received: ${repo}`); + } + + return { + owner, + repo: name, + fullName: `${owner}/${name}`, + clusterRank: Number.isFinite(clusterRank) ? Math.max(1, clusterRank) : 1, + backend: backend === 'exact' ? 'exact' : 'vectorlite', + k: Number.isFinite(k) ? k : undefined, + threshold: Number.isFinite(threshold) ? threshold : undefined, + candidateK: Number.isFinite(candidateK) ? candidateK : undefined, + efSearch: Number.isFinite(efSearch) ? efSearch : undefined, + topSubclusters: Number.isFinite(topSubclusters) ? Math.max(1, topSubclusters) : 10, + }; +} + +function edgeKey(leftThreadId, rightThreadId) { + const [left, right] = leftThreadId < rightThreadId ? [leftThreadId, rightThreadId] : [rightThreadId, leftThreadId]; + return `${left}:${right}`; +} + +function mergeSourceKindEdges(aggregated, edges) { + for (const edge of edges) { + const key = edgeKey(edge.leftThreadId, edge.rightThreadId); + const existing = aggregated.get(key); + if (existing) { + existing.score = Math.max(existing.score, edge.score); + continue; + } + aggregated.set(key, { + leftThreadId: edge.leftThreadId, + rightThreadId: edge.rightThreadId, + score: edge.score, + }); + } +} + +function loadThreadMeta(service, ids) { + const placeholders = ids.map(() => '?').join(', '); + const rows = service.db + .prepare( + `select id, number, kind, title + from threads + where id in (${placeholders})`, + ) + .all(...ids); + return new Map(rows.map((row) => [row.id, row])); +} + +function normalizeEmbedding(values) { + let normSquared = 0; + for (const value of values) { + normSquared += value * value; + } + const norm = Math.sqrt(normSquared); + if (norm === 0) { + return values.map(() => 0); + } + return values.map((value) => value / norm); +} + +function normalizeRows(rows) { + return rows.map((row) => ({ + id: row.id, + normalizedEmbedding: normalizeEmbedding(JSON.parse(row.embedding_json)), + })); +} + +function describeThread(threadId, metaById) { + const meta = metaById.get(threadId); + if (!meta) { + return `thread:${threadId}`; + } + const kind = meta.kind === 'pull_request' ? 'PR' : 'Issue'; + return `${kind} #${meta.number} ${meta.title}`; +} + +const args = parseArgs(process.argv.slice(2)); +const service = new GHCrawlService(); + +try { + const result = service.clusterExperiment({ + owner: args.owner, + repo: args.repo, + backend: args.backend, + k: args.k, + minScore: args.threshold, + candidateK: args.candidateK, + efSearch: args.efSearch, + includeClusters: true, + onProgress: (message) => process.stdout.write(`${message}\n`), + }); + + const rankedClusters = [...(result.clustersDetail ?? [])].sort( + (left, right) => right.memberThreadIds.length - left.memberThreadIds.length || left.representativeThreadId - right.representativeThreadId, + ); + const selectedCluster = rankedClusters[args.clusterRank - 1]; + if (!selectedCluster) { + throw new Error(`Cluster rank ${args.clusterRank} not found`); + } + + const repository = service.requireRepository(args.owner, args.repo); + const ids = [...selectedCluster.memberThreadIds]; + const metaById = loadThreadMeta(service, ids); + const sourceKinds = service.db + .prepare( + `select distinct e.source_kind as sourceKind + from document_embeddings e + join threads t on t.id = e.thread_id + where t.repo_id = ? + and t.id in (${ids.map(() => '?').join(', ')}) + and e.model = ? + order by e.source_kind asc`, + ) + .all(repository.id, ...ids, service.config.embedModel) + .map((row) => row.sourceKind); + + const aggregated = new Map(); + for (const sourceKind of sourceKinds) { + const rows = service.db + .prepare( + `select t.id, e.embedding_json + from document_embeddings e + join threads t on t.id = e.thread_id + where t.repo_id = ? + and t.id in (${ids.map(() => '?').join(', ')}) + and e.model = ? + and e.source_kind = ?`, + ) + .all(repository.id, ...ids, service.config.embedModel, sourceKind); + const normalizedRows = normalizeRows(rows); + const edges = buildSourceKindEdges(normalizedRows, { + limit: args.k ?? 6, + minScore: args.threshold ?? 0.82, + }); + mergeSourceKindEdges(aggregated, edges); + } + + const refinedClusters = buildClusters( + ids.map((threadId) => { + const meta = metaById.get(threadId); + return { + threadId, + number: meta?.number ?? threadId, + title: meta?.title ?? '', + }; + }), + Array.from(aggregated.values()), + ); + + const lines = [ + '## Refined Cluster', + '', + `- Repo: ${args.fullName}`, + `- Source backend cluster: ${args.backend}`, + `- Source cluster rank: ${args.clusterRank}`, + `- Source cluster size: ${selectedCluster.memberThreadIds.length}`, + `- Representative: ${describeThread(selectedCluster.representativeThreadId, metaById)}`, + `- Exact refined subclusters: ${refinedClusters.length}`, + '', + '### Refined Sizes', + '', + ]; + + for (const [index, cluster] of refinedClusters.slice(0, args.topSubclusters).entries()) { + lines.push( + `- #${index + 1} size=${cluster.members.length} representative=${describeThread(cluster.representativeThreadId, metaById)}`, + ); + } + + process.stdout.write(`\n${lines.join('\n')}\n`); +} finally { + service.close(); +} diff --git a/scripts/cluster-topology-compare.mjs b/scripts/cluster-topology-compare.mjs index 3ce8ca8..c5080b6 100644 --- a/scripts/cluster-topology-compare.mjs +++ b/scripts/cluster-topology-compare.mjs @@ -16,6 +16,7 @@ function parseArgs(argv) { let childBackend = null; let top = 5; let sampleMembers = 12; + let efSearch; for (let index = 0; index < argv.length; index += 1) { const token = argv[index]; @@ -40,6 +41,11 @@ function parseArgs(argv) { index += 1; continue; } + if (token === '--ef-search') { + efSearch = Number(argv[index + 1]); + index += 1; + continue; + } if (token === '--child-backend') { childBackend = argv[index + 1] ?? null; index += 1; @@ -72,6 +78,7 @@ function parseArgs(argv) { k: Number.isFinite(k) ? k : undefined, threshold: Number.isFinite(threshold) ? threshold : undefined, candidateK: Number.isFinite(candidateK) ? candidateK : undefined, + efSearch: Number.isFinite(efSearch) ? efSearch : undefined, childBackend, top: Number.isFinite(top) ? Math.max(1, top) : 5, sampleMembers: Number.isFinite(sampleMembers) ? Math.max(1, sampleMembers) : 12, @@ -88,6 +95,7 @@ async function runChild(args) { k: args.k, minScore: args.threshold, candidateK: args.candidateK, + efSearch: args.efSearch, includeClusters: true, onProgress: (message) => process.stdout.write(`${message}\n`), }); @@ -119,6 +127,9 @@ async function runBackend(backend, args) { if (args.candidateK !== undefined) { childArgs.push('--candidate-k', String(args.candidateK)); } + if (args.efSearch !== undefined) { + childArgs.push('--ef-search', String(args.efSearch)); + } const child = spawn(process.execPath, childArgs, { cwd: repoRoot, @@ -361,6 +372,7 @@ async function runParent(args) { '', `- Repo: ${args.fullName}`, `- Parameters: k=${args.k ?? 'default'} threshold=${args.threshold ?? 'default'} candidateK=${args.candidateK ?? 'default'}`, + `- Vectorlite efSearch: ${args.efSearch ?? 'default(10)'}`, `- Exact clusters: ${exactResult.clusters}`, `- Vectorlite clusters: ${vectorliteResult.clusters}`, '', From a14a3d52ac2ab418b16e9bd756aeda76dc758ff8 Mon Sep 17 00:00:00 2001 From: huntharo Date: Mon, 30 Mar 2026 16:09:26 -0400 Subject: [PATCH 17/34] feat: optimize summary prompt and add concurrent summarization pipeline Tested 11 system prompt variants with LLM-as-judge on 40 threads. Winner (v5-component-focused) scores 4.97/5 vs baseline 2.65/5 with 0% boilerplate and 100% clustering correctness. - Replace sequential summarize loop with two-stage IterableMapper pipeline (concurrency 5 for API calls, concurrency 1 for DB writes) - Add running cost estimate and ETA to summarize progress output - Add op-run.mjs 'run' mode for arbitrary commands with 1Password env - Add experiment scripts for prompt optimization Co-Authored-By: Claude Opus 4.6 --- .../summary-prompt/experiment-log.yaml | 65 ++ .../summary-prompt/prompts/v0-baseline.txt | 1 + .../prompts/v1-strip-boilerplate.txt | 16 + .../prompts/v10-minimal-rules.txt | 3 + .../prompts/v2-signal-focused.txt | 20 + .../summary-prompt/prompts/v3-examples.txt | 19 + .../summary-prompt/prompts/v4-terse.txt | 3 + .../prompts/v5-component-focused.txt | 12 + .../prompts/v6-negative-examples.txt | 16 + .../summary-prompt/prompts/v7-two-pass.txt | 19 + .../prompts/v8-embedding-aware.txt | 19 + .../summary-prompt/prompts/v9-issue-vs-pr.txt | 16 + .../summary-prompt/results/v0-baseline.json | 940 ++++++++++++++++++ .../results/v1-strip-boilerplate.json | 940 ++++++++++++++++++ .../results/v10-minimal-rules.json | 940 ++++++++++++++++++ .../results/v2-signal-focused.json | 940 ++++++++++++++++++ .../summary-prompt/results/v3-examples.json | 940 ++++++++++++++++++ .../summary-prompt/results/v4-terse.json | 940 ++++++++++++++++++ .../results/v5-component-focused.json | 940 ++++++++++++++++++ .../results/v6-negative-examples.json | 940 ++++++++++++++++++ .../summary-prompt/results/v7-two-pass.json | 940 ++++++++++++++++++ .../results/v8-embedding-aware.json | 940 ++++++++++++++++++ .../results/v9-issue-vs-pr.json | 940 ++++++++++++++++++ .../summary-prompt/test-threads.yaml | 92 ++ .../vectorlite-clustering/experiment-log.yaml | 225 +++++ packages/api-core/src/openai/provider.ts | 16 +- packages/api-core/src/service.ts | 116 ++- scripts/cluster-optimize-measure.mjs | 136 +++ scripts/op-run.mjs | 9 + scripts/run-all-prompt-experiments.mjs | 69 ++ scripts/summarize-prompt-experiment.mjs | 275 +++++ scripts/summarize-single.mjs | 145 +++ 32 files changed, 11604 insertions(+), 28 deletions(-) create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/experiment-log.yaml create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v0-baseline.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v1-strip-boilerplate.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v10-minimal-rules.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v2-signal-focused.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v3-examples.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v4-terse.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v5-component-focused.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v6-negative-examples.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v7-two-pass.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v8-embedding-aware.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v9-issue-vs-pr.txt create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v0-baseline.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v1-strip-boilerplate.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v10-minimal-rules.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v2-signal-focused.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v3-examples.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v4-terse.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v5-component-focused.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v6-negative-examples.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v7-two-pass.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v8-embedding-aware.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v9-issue-vs-pr.json create mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/test-threads.yaml create mode 100644 .context/compound-engineering/ce-optimize/vectorlite-clustering/experiment-log.yaml create mode 100644 scripts/cluster-optimize-measure.mjs create mode 100644 scripts/run-all-prompt-experiments.mjs create mode 100644 scripts/summarize-prompt-experiment.mjs create mode 100644 scripts/summarize-single.mjs diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/experiment-log.yaml b/.context/compound-engineering/ce-optimize/summary-prompt/experiment-log.yaml new file mode 100644 index 0000000..b150e5a --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/experiment-log.yaml @@ -0,0 +1,65 @@ +summary_prompt_optimization: + completed: 2026-03-30T18:40:59.650Z + winner: v5-component-focused + runner_up: v2-signal-focused + baseline_score: 2.65 + winner_score: 4.97 + improvement: 87.5% + total_experiments: 11 + test_threads: 40 + results: + - id: v2-signal-focused + avg_score: 4.97 + boilerplate_pct: 0 + cluster_correct_pct: 100 + input_tokens: 47983 + - id: v5-component-focused + avg_score: 4.97 + boilerplate_pct: 0 + cluster_correct_pct: 100 + input_tokens: 45423 + - id: v1-strip-boilerplate + avg_score: 4.95 + boilerplate_pct: 0 + cluster_correct_pct: 100 + input_tokens: 46623 + - id: v3-examples + avg_score: 4.95 + boilerplate_pct: 0 + cluster_correct_pct: 100 + input_tokens: 49863 + - id: v7-two-pass + avg_score: 4.93 + boilerplate_pct: 3 + cluster_correct_pct: 100 + input_tokens: 48503 + - id: v6-negative-examples + avg_score: 4.78 + boilerplate_pct: 3 + cluster_correct_pct: 100 + input_tokens: 47183 + - id: v9-issue-vs-pr + avg_score: 4.75 + boilerplate_pct: 3 + cluster_correct_pct: 100 + input_tokens: 46676 + - id: v8-embedding-aware + avg_score: 4.58 + boilerplate_pct: 5 + cluster_correct_pct: 100 + input_tokens: 47543 + - id: v4-terse + avg_score: 4.47 + boilerplate_pct: 3 + cluster_correct_pct: 100 + input_tokens: 41983 + - id: v10-minimal-rules + avg_score: 4.1 + boilerplate_pct: 25 + cluster_correct_pct: 95 + input_tokens: 40703 + - id: v0-baseline + avg_score: 2.65 + boilerplate_pct: 28 + cluster_correct_pct: 43 + input_tokens: 40423 diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v0-baseline.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v0-baseline.txt new file mode 100644 index 0000000..8069562 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v0-baseline.txt @@ -0,0 +1 @@ +Summarize this GitHub issue or pull request thread. Return concise JSON only with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field should be plain text, no markdown, and usually 1-3 sentences. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v1-strip-boilerplate.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v1-strip-boilerplate.txt new file mode 100644 index 0000000..4844293 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v1-strip-boilerplate.txt @@ -0,0 +1,16 @@ +Summarize this GitHub issue or pull request. Your summary will be embedded as a vector and used for duplicate/similar-issue detection via clustering. + +IGNORE and strip away: +- Issue/PR template sections (### Bug type, ### Steps to reproduce, ### Expected behavior, ### Checklist, etc.) +- Testing instructions, QA notes, deployment steps +- Checkbox lists and checklists +- Version numbers, OS details, and environment info (unless they ARE the bug) +- "Related issues" or "References" sections +- Auto-generated content + +FOCUS on extracting: +- The core problem or feature being described +- Specific technical components, APIs, or subsystems affected +- The root cause or proposed solution (if stated) + +Return concise JSON with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field: plain text, no markdown, 1-3 sentences. The dedupe_summary should capture the essential topic in a way that would match other reports of the same issue. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v10-minimal-rules.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v10-minimal-rules.txt new file mode 100644 index 0000000..f94b9b5 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v10-minimal-rules.txt @@ -0,0 +1,3 @@ +Summarize this GitHub issue/PR for duplicate detection. Strip template boilerplate and testing notes. Focus on the specific technical problem or change. + +Return JSON: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Plain text, 1-3 sentences each. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v2-signal-focused.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v2-signal-focused.txt new file mode 100644 index 0000000..05d8763 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v2-signal-focused.txt @@ -0,0 +1,20 @@ +You are extracting the core signal from a GitHub issue or pull request for duplicate detection. The output will be embedded and clustered with similar issues. + +Your job is to answer: "What is this issue/PR fundamentally about?" Ignore everything else. + +Strip away ALL of the following — they add noise to embeddings: +- Template boilerplate (headings like "### Steps to reproduce", "### Expected behavior") +- Testing notes, QA instructions, review checklists +- Environment details (OS, browser, versions) unless they ARE the core issue +- Deployment procedures, rollback plans +- Links, references to other issues +- Formatting artifacts (checkboxes, horizontal rules, badges) + +What to capture: +- The specific bug, feature, or change in concrete terms +- Which component/module/API/subsystem is affected +- Why it matters (the user impact, not the testing impact) + +Return JSON with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. +Rules: plain text only, no markdown, 1-3 sentences each. +dedupe_summary is the most important field — it must be a clean, specific description that would match other reports about the same underlying issue. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v3-examples.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v3-examples.txt new file mode 100644 index 0000000..5720eeb --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v3-examples.txt @@ -0,0 +1,19 @@ +Summarize this GitHub issue or pull request for duplicate detection clustering. The dedupe_summary will be embedded as a vector — it must be clean signal with zero noise. + +REMOVE all template noise. Examples of what to strip: +- "### Bug type\nRegression (worked before, now fails)" → ignore, it's a template field +- "### Steps to reproduce\n1. Go to settings\n2. Click..." → ignore reproduction steps +- "### Expected behavior\nIt should work" → ignore +- "- [x] Tests added\n- [x] Docs updated" → ignore checklist +- "### Environment\nOS: macOS 14.2, Node 20.1" → ignore unless the OS IS the bug +- "Fixes #1234" → ignore cross-references +- "## Testing\nRun `pnpm test`..." → ignore testing instructions + +KEEP the core signal: +- "Discord REST API ignores proxy configuration" → THIS is the signal +- "WhatsApp group messages not delivered when streaming enabled" → THIS is the signal +- "Telegram media download hangs on large video files" → THIS is the signal + +Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. +Plain text, no markdown, 1-3 sentences each. +dedupe_summary must be a specific, searchable description of the core issue or change. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v4-terse.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v4-terse.txt new file mode 100644 index 0000000..08b7a54 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v4-terse.txt @@ -0,0 +1,3 @@ +Extract the core topic from this GitHub issue or PR for duplicate clustering. Ignore all template boilerplate, testing notes, checklists, environment details, and reproduction steps. Focus only on WHAT the issue/change is about technically. + +Return JSON: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Plain text, 1-2 sentences each. Be extremely concise — every word in dedupe_summary must add signal for finding duplicates. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v5-component-focused.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v5-component-focused.txt new file mode 100644 index 0000000..6ccdab9 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v5-component-focused.txt @@ -0,0 +1,12 @@ +Summarize this GitHub issue or pull request for automated duplicate detection. Your summary will be embedded and clustered. + +Structure your analysis: +1. First identify the COMPONENT or SUBSYSTEM (e.g., "Discord gateway", "WhatsApp delivery", "Telegram media handler", "CLI routing", "session management") +2. Then identify the SPECIFIC PROBLEM or CHANGE within that component +3. Combine into a clear dedupe_summary that starts with the component name + +Ignore completely: template boilerplate, testing instructions, checklists, environment info, reproduction steps, deployment notes, version numbers, cross-references. + +Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. +Plain text, no markdown, 1-3 sentences each. +dedupe_summary format: "[Component]: [specific issue or change]" — this helps cluster by subsystem. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v6-negative-examples.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v6-negative-examples.txt new file mode 100644 index 0000000..c98f3c7 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v6-negative-examples.txt @@ -0,0 +1,16 @@ +Summarize this GitHub issue or pull request. The dedupe_summary field will be used for vector-based duplicate detection. + +BAD dedupe_summary examples (too generic, would match unrelated issues): +- "Bug fix for an issue with the application" ← too vague +- "Updates configuration handling and fixes a reported problem" ← no specifics +- "Resolves issue where feature doesn't work as expected" ← says nothing + +GOOD dedupe_summary examples (specific, would cluster correctly): +- "Discord REST API requests bypass proxy configuration, causing fetch failures in proxy-required environments" +- "WhatsApp group-level systemPrompt configuration not applied to message delivery" +- "Telegram editMessageCaption fails for non-photo media types due to missing content-type detection" + +Remove from your analysis: template headers, checklists, testing instructions, environment details, reproduction steps, deployment notes. These add noise to embeddings. + +Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. +Plain text, no markdown, 1-3 sentences each. dedupe_summary is the most critical field. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v7-two-pass.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v7-two-pass.txt new file mode 100644 index 0000000..f02325d --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v7-two-pass.txt @@ -0,0 +1,19 @@ +You will summarize a GitHub issue or pull request in two mental passes: + +PASS 1 — STRIP: Mentally remove all of the following before summarizing: +- Template sections: headers like "### Bug type", "### Steps to reproduce", "### Checklist", "## Testing" +- Checkboxes: "- [x] Tests added", "- [ ] Docs updated" +- Environment details: OS versions, browser versions, Node versions +- Reproduction steps: numbered step lists +- Testing instructions: "Run `npm test`", "Deploy to staging" +- Cross-references: "Fixes #1234", "Related to #5678" +- Formatting: horizontal rules, badges, images unless they show the bug + +PASS 2 — EXTRACT: From what remains, identify: +- What specific component or API is affected? +- What exactly goes wrong or what change is being made? +- What is the user-facing impact? + +Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. +All fields: plain text, no markdown, 1-3 sentences. +dedupe_summary: the essential topic that would match other reports of the same issue. Be specific enough to distinguish from similar-but-different issues. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v8-embedding-aware.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v8-embedding-aware.txt new file mode 100644 index 0000000..9051092 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v8-embedding-aware.txt @@ -0,0 +1,19 @@ +This text is a GitHub issue or pull request. You must produce a summary optimized for text embedding and cosine-similarity-based duplicate detection. + +Key insight: the dedupe_summary will be converted to a 3072-dimensional vector. Similar vectors cluster together. Your goal is to maximize the chance that reports about the SAME underlying issue produce SIMILAR vectors, while reports about DIFFERENT issues produce DISSIMILAR vectors. + +What helps similarity detection: +- Specific technical terms (API names, component names, error messages) +- The core behavior being reported or changed +- Affected subsystem or module names + +What hurts similarity detection (creates false matches or misses): +- Generic words: "bug", "fix", "issue", "problem", "update", "change" +- Template boilerplate that appears in many unrelated issues +- Testing details, reproduction steps, environment info +- Version numbers, dates, PR metadata + +Strip all boilerplate. Extract only the technical signal. + +Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. +Plain text, no markdown, 1-3 sentences each. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v9-issue-vs-pr.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v9-issue-vs-pr.txt new file mode 100644 index 0000000..c982729 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v9-issue-vs-pr.txt @@ -0,0 +1,16 @@ +Summarize this GitHub issue or pull request for duplicate detection clustering. + +If this is an ISSUE: +- Extract: What specific problem is reported? Which component/API/feature is broken? +- Ignore: reproduction steps, expected vs actual behavior headings, environment details, template fields +- dedupe_summary should describe THE PROBLEM in specific technical terms + +If this is a PULL REQUEST: +- Extract: What specific problem does this fix? What change is being made and to what component? +- Ignore: testing instructions, review checklists, deployment notes, CI status, diff statistics +- dedupe_summary should describe THE FIX/CHANGE and what problem it addresses + +In both cases, strip template boilerplate, checklists, and any content that would appear identically in unrelated issues/PRs. + +Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. +Plain text, no markdown, 1-3 sentences each. dedupe_summary is most important — it must uniquely identify this topic. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v0-baseline.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v0-baseline.json new file mode 100644 index 0000000..0a99446 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v0-baseline.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v0-baseline", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "Summarize this GitHub issue or pull request thread. Return concise JSON only with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field should be plain text, no markdown, and usually 1-3 sentences.", + "timestamp": "2026-03-30T15:44:34.227Z", + "aggregate": { + "avg_score": 2.65, + "avg_signal_density": 2.65, + "boilerplate_count": 11, + "boilerplate_pct": 28, + "would_cluster_correctly_pct": 43, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 40423, + "total_output_tokens": 24956 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "channels.discord.proxy only proxies the Discord WebSocket gateway; startup REST API calls (deploy commands, fetch bot identity) and the guild resolution path do not use the configured HTTP proxy, causing REST requests to time out and guild channels to be non-functional in environments that require a proxy.", + "solution_summary": "Apply the configured proxy to the undici HTTP dispatcher for all Discord REST calls (not just the gateway), ensure the guild-resolve code path uses the same proxied HTTP client and add retry logic; optionally add a global proxy config (e.g., gateway.proxy or network.proxy) to set a global ProxyAgent.", + "maintainer_signal_summary": "Issue labeled bug and regression; reporter provided logs, reproduction steps, and a partial workaround (global ProxyAgent) that fixes REST but not guild resolve; no maintainer comments or fixes in the thread yet.", + "dedupe_summary": "No duplicate referenced in the thread; this appears to be a regression distinct from WebSocket proxying because guild-resolve uses a different HTTP path — check other proxy-related issues but treat this as a new, actionable bug." + }, + "judge": { + "score": 3, + "has_boilerplate": true, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Identifies the core regression (REST/guild-resolve not proxied vs WebSocket) but includes meta/instructional noise ('No duplicate referenced', 'check other proxy-related issues') that reduces clarity and conciseness for deduplication." + }, + "usage": { + "input_tokens": 2146, + "output_tokens": 556 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "Outbound Discord REST requests are not using the configured channels.discord.proxy, so sending messages/attachments fails with fetch failed in restricted networks while the WebSocket gateway (receiving) works via the proxy.", + "solution_summary": "Add proxy support to the Discord RequestClient/fetch implementation (or pass a proxy agent to undici/node fetch) so all REST calls and media uploads/downloads honor channels.discord.proxy; add tests and docs for proxy behavior.", + "maintainer_signal_summary": "No maintainer responses in the thread; only a user bug report (labelled bug) with environment/version details and reproduction steps.", + "dedupe_summary": "Likely duplicates other requests for Discord REST/RequestClient proxy support or undici fetch proxying; search existing issues for \"discord proxy\", \"RequestClient proxy\", or \"undici proxy\" before filing a new one." + }, + "judge": { + "score": 3, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Mentions the relevant components (Discord REST/RequestClient and undici proxy support) but reads like a duplicate suggestion rather than a concise statement that outbound REST requests aren't using the configured proxy, so it has useful keywords but misses the core failure detail." + }, + "usage": { + "input_tokens": 828, + "output_tokens": 584 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "When OpenClaw is configured to use an HTTP proxy for Discord, the provider can connect to the gateway and log in and can fetch the application ID, but some startup REST calls (deploying native commands and fetching bot identity) consistently fail with \"fetch failed\", leaving the Discord provider partially functional and triggering periodic stale-socket restarts.", + "solution_summary": "No confirmed fix in thread; suspected root cause is inconsistent proxy handling where some Discord REST requests bypass the proxy or use a different fetch/dispatcher path. Recommended action is to ensure all REST calls use the same proxy-aware fetch path; the reporter offered to provide more details or test a patch.", + "maintainer_signal_summary": "No maintainer responses in the thread; the reporter supplied logs, environment details, reproduction notes, and offered to assist with testing.", + "dedupe_summary": "Not marked as a duplicate; distinct because the gateway/login succeeds while specific REST startup calls fail, indicating inconsistent proxy usage rather than token/config errors; should be compared to other proxy-related reports but appears unique. " + }, + "judge": { + "score": 3, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "The summary correctly highlights the core issue (gateway login succeeds while REST startup calls fail due to inconsistent proxy usage) but includes meta phrasing and omits the specific failing REST operations (native command deployment, bot identity fetch), so it's adequate for proxy-related clustering but could be more precise." + }, + "usage": { + "input_tokens": 974, + "output_tokens": 549 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "OpenClaw reports Discord proxy as enabled but REST API calls fail with \"fetch failed\" under WSL2 using an HTTP proxy; WebSocket/gateway never establishes though curl via the same proxy succeeds. The Discord channel is thus completely non-functional despite correct proxy config in ~/.openclaw/openclaw.json.", + "solution_summary": "No fix provided in the thread. Reporter notes the proxy appears logged but not applied to the underlying Discord.js/fetch client; suggested workarounds are using alternative channels (Feishu) or a TUN-mode transparent proxy.", + "maintainer_signal_summary": "No maintainer response in the thread. The reporter supplied reproducible steps, logs, config, environment (OpenClaw 2026.3.13 on WSL2), and labeled severity as high.", + "dedupe_summary": "No duplicates referenced; issue appears unique and 100% reproducible when an HTTP proxy is required, likely needing a code change to ensure the proxy agent is passed to Discord.js/fetch clients." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary that captures the core problem — Discord proxy is recognized but REST calls fail because the proxy agent isn't passed to Discord.js/fetch — and suggests the needed code change, with minimal noise." + }, + "usage": { + "input_tokens": 811, + "output_tokens": 696 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "Discord REST API calls from OpenClaw ignore the configured proxy and fail with \"TypeError: fetch failed\" when sending messages, while the WebSocket connection and curl via the same proxy work; Telegram and Discord WebSocket honor the proxy so the issue is specific to Discord REST requests.", + "solution_summary": "Make Discord REST requests use the same proxy-aware SSRF-guarded fetch path implemented for web_search/web_fetch so they honor channels.discord.proxy and HTTP(S)_PROXY environment variables.", + "maintainer_signal_summary": "No maintainer response in the thread; reporter points to the 2026.2.26 changelog that fixed web_tools proxying but did not include Discord REST and requests that Discord REST be included in the fix; a webhook workaround was suggested.", + "dedupe_summary": "Related to the 2026.2.26 web_tools proxy fix but not a duplicate; should be tracked alongside web_fetch/web_search proxy issues and any other channel REST proxy reports." + }, + "judge": { + "score": 2, + "has_boilerplate": false, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "The summary is meta (references a specific release and related fixes) rather than stating the core bug (Discord REST API ignoring proxy and fetch failures), so it lacks the concrete problem description needed for reliable deduplication." + }, + "usage": { + "input_tokens": 834, + "output_tokens": 745 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "In v2026.3.11 the CLI command 'openclaw agent --channel discord --to ...' routes deliveries to agent:*:main instead of the live Discord channel/thread session because the session resolver does not search by delivery context, the command path omits channel/account context, Discord targets are not normalized, and the compact resolver lookup is incorrect.", + "solution_summary": "A manual patch was applied to compiled dist files to add delivery-context-based session resolution, normalize Discord targets, pass channel and replyAccount/accountId through the command path, and fix the compact resolver; post-patch validation shows messages route to the correct channel/thread session.", + "maintainer_signal_summary": "This is high-risk because the fix was applied to compiled dist files and will be lost on updates; maintainers should implement the fix in source, add tests, and include a post-update revalidation step. Affected compiled files and related issue numbers are listed in the report.", + "dedupe_summary": "Likely a regression related to existing reports about session routing fallbacks and Discord thread leakage (issues #36401, #41562, #16149); consider de-duplicating and tracking under a canonical regression issue. " + }, + "judge": { + "score": 2, + "has_boilerplate": true, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "The summary is too generic and meta—it only flags this as a regression and links other issues instead of describing the specific CLI session-resolver bug (delivery-context lookup, Discord target normalization), so it won't reliably cluster similar technical reports." + }, + "usage": { + "input_tokens": 761, + "output_tokens": 765 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions affecting Discord live sessions: volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) was re-injected every turn causing context bloat and degradation, and CLI commands routed to the wrong session (defaulting to agent:*:main instead of the thread-specific session).", + "solution_summary": "Patched compiled runtime bundles to stop reinjecting thread metadata, added session resolution by actual delivery target and delivery-context lookup, propagated channel and replyAccount/accountId to the resolver, and corrected use of buildAgentMainSessionKey. Service was restarted and sessions verified to show correct toolCall outputs and no repeated context.", + "maintainer_signal_summary": "Changes were applied as manual edits to compiled bundles in openclaw/dist and verified by restarting openclaw-gateway.service; maintainers note this patch will be lost on update and recommend keeping a re-application script. Environment: OpenClaw 2026.3.11, Discord channel, systemd deployment.", + "dedupe_summary": "Treat as a single issue with two related root causes (runtime metadata injection and CLI resolver context); track by affected files (pi-embedded, gateway-cli, compact, register.agent) and symptoms (context bloat in threads, CLI commands landing in main). Mark as high priority and avoid reopening duplicates that describe the same Discord-thread metadata reinjection or CLI routing behavior." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise summary that captures both core regressions (volatile metadata re-injection in Discord threads and CLI session resolver routing), affected components, and symptoms, with only a minor non-deduplication directive that isn't necessary for clustering." + }, + "usage": { + "input_tokens": 680, + "output_tokens": 620 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "The sandbox media staging limit is hardcoded to 5 MB in dist/store-*.js (MEDIA_MAX_BYTES = 5 * 1024 * 1024), causing files larger than 5 MB to be rejected or silently fail in sandboxed agents. This regression exists in v2026.3.7 and v2026.3.8 and requires a manual post-install patch as a workaround.", + "solution_summary": "Expose the MEDIA_MAX_BYTES as a configurable option (e.g., tools.media.maxBytes in openclaw.json) or read the limit from existing per-type config, remove the hardcoded constant from built files, and apply a sensible default when unset so larger files can be allowed per user configuration.", + "maintainer_signal_summary": "Issue is labeled bug and regression and includes version, OS, repro steps and a workaround, but there are no maintainer comments or fixes in the thread yet.", + "dedupe_summary": "Likely unique but could overlap with other issues about media size limits or config not honored; check for prior issues mentioning MEDIA_MAX_BYTES or sandbox media size to avoid duplication." + }, + "judge": { + "score": 1, + "has_boilerplate": false, + "signal_density": 1, + "would_cluster_correctly": false, + "reasoning": "The summary fails to state the core bug (sandbox MEDIA_MAX_BYTES hardcoded to 5MB and not user-configurable) and instead gives meta guidance about uniqueness, so it’s noisy and won’t help cluster similar issues." + }, + "usage": { + "input_tokens": 727, + "output_tokens": 547 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "Discord bot can receive messages via the Gateway but cannot send them: all REST API calls fail with 'fetch failed' despite proxy configured in openclaw.json, gateway logs showing 'rest proxy enabled', and curl requests using the same proxy succeeding. Environment: OpenClaw 2026.2.25 on macOS with Node.js 25.6.1.", + "solution_summary": "No resolution recorded in the thread. Suggested next actions are to verify OpenClaw actually applies the configured proxy to its REST client (and whether the underlying HTTP client supports proxies), try setting HTTP_PROXY/HTTPS_PROXY environment variables, collect full stack traces and debug logs, and reproduce a failing REST call from Node to isolate whether undici/fetch is bypassing the proxy.", + "maintainer_signal_summary": "Gateway logs indicate 'rest proxy enabled' and 'gateway proxy enabled' but also show repeated 'fetch failed' errors and failed native command deployment/fetching bot identity; no maintainer responses or fixes are included in the thread.", + "dedupe_summary": "No duplicates or related issues referenced in the thread; appears to be a new bug report specific to REST requests and proxy handling." + }, + "judge": { + "score": 2, + "has_boilerplate": true, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "The summary is mostly metadata ('No duplicates...') and too generic—it fails to mention Discord, the 'fetch failed' error, or that REST requests via configured proxy are failing—so it won't reliably cluster similar issues." + }, + "usage": { + "input_tokens": 672, + "output_tokens": 810 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "@buape/carbon's RequestClient (and other globalThis.fetch call sites like memorySearch) do not honor channels.discord.proxy from openclaw.json, so REST API calls and some media downloads bypass the configured proxy while the WebSocket gateway correctly uses it. The root cause is RequestClient calling globalThis.fetch without a proxy-aware undici dispatcher and Node 22's native fetch not using env proxy vars by default.", + "solution_summary": "Change @buape/carbon's REST client to accept an optional dispatcher/proxy configuration and have OpenClaw pass channels.discord.proxy into it so REST calls use the same proxy as the gateway. Short-term workaround: preload undici's EnvHttpProxyAgent via NODE_OPTIONS to set a global dispatcher.", + "maintainer_signal_summary": "No maintainer responses or actions recorded in the thread; the report includes a clear bug description, root cause analysis, a workaround, and an explicit suggested fix.", + "dedupe_summary": "Related to issue #25232 (attachment downloads bypass proxy) which shares the same root cause but affects a different code path; consider linking or consolidating fixes rather than duplicating efforts." + }, + "judge": { + "score": 2, + "has_boilerplate": false, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "The summary only references a related issue and suggests consolidation without describing the actual technical problem (Discord REST API bypassing channels.discord.proxy), so it lacks the specific signal needed for reliable deduplication." + }, + "usage": { + "input_tokens": 681, + "output_tokens": 660 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "Under sustained load and long-running conversations OpenClaw experiences unbounded context token growth, increasing per-turn latency, Discord lane blocking/serialization under load, and typing indicators stuck until TTL in some paths.", + "solution_summary": "Applied configuration limits and timeouts: reduced concurrent session/agent/subagent limits, set agents.defaults.contextTokens to 80000, increased Discord eventQueue.listenerTimeout to 300000, improved patching script to cover hash variants, and restarted the gateway; post-restart no new lane-wait or typing-TTL errors observed.", + "maintainer_signal_summary": "Maintainers implemented and validated the config changes and hardening script, restarted the gateway, and confirmed the immediate errors stopped while noting restart is a mitigation not a full fix; they recommended making limits adaptive or exposing them as tunable config options.", + "dedupe_summary": "Related issues #27690 (lane blocking), #44026 (serialization by agentId), and #26733/#27172 (typing TTL) overlap with this root-cause; treat this thread as the umbrella/root fix and link or mark the related issues as duplicates or dependent. " + }, + "judge": { + "score": 2, + "has_boilerplate": false, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "The summary is an action note about linking related issue IDs rather than a concise description of the core problem (unbounded context growth, lane blocking, typing TTL under load), so it provides low useful signal for deduplication and won't reliably cluster similar technical reports." + }, + "usage": { + "input_tokens": 615, + "output_tokens": 719 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, causing context contamination, token bloat, prompt cache busting and degraded performance. The compiled runtime lacks a guard to limit this injection to the first turn, unlike prior Slack/iMessage fixes.", + "solution_summary": "A manual patch to compiled dist files prevents repeated injection and adds an isNewSession guard: dist/pi-embedded-D6PpOsxP.js stops injecting the listed metadata, and dist/compact-1mmJ_KWL.js adds the isNewSession check and replaces resolveAgentMainSessionKey with buildAgentMainSessionKey. Post-patch validation shows threads no longer repeat context and prompt caching is restored.", + "maintainer_signal_summary": "High regression risk because fixes were applied directly to compiled dist/ files and will be overwritten by updates; maintainers should apply the fix in source, include tests/guards, and add a post-update revalidation script. Issue is urgent and related to several open threads.", + "dedupe_summary": "Related issues appear to be duplicates or closely related (#41355, #34228, #37047) and should be consolidated under a single upstream fix for Discord thread metadata injection to avoid repeated reports." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Clearly identifies the core problem (Discord thread metadata injection) and intent to consolidate duplicates, but omits specific technical symptoms (which fields, cache/token impact) and includes issue-number noise." + }, + "usage": { + "input_tokens": 627, + "output_tokens": 530 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord thread messages repeatedly re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, causing context pollution, prompt cache misses, increased token usage and degraded performance in long threads. The compiled runtime in dist/pi-embedded-D6PpOsxP.js treats Discord like other platforms but lacks guards limiting injection to the first turn.", + "solution_summary": "A local manual patch to dist/pi-embedded-D6PpOsxP.js stops injecting the volatile Discord metadata after the first turn; post-patch validation shows eliminated repeated context, restored prompt caching, and reduced token usage. The change was applied directly to compiled files and verified, but not yet merged into source.", + "maintainer_signal_summary": "Reporter applied a high-risk manual fix and requests an official source-level patch plus a post-update revalidation script because edits to dist/ will be overwritten by updates. No maintainer merge or official release yet; action recommended is to implement the fix in source and release a patched build.", + "dedupe_summary": "Related/overlapping issues: #41355 (ThreadStarterBody re-injected), #34228 (thread sessions reset), #37047 (starter metadata busting cache). This appears to be the same root cause as prior Slack/iMessage fixes and should be linked or deduplicated with those tickets." + }, + "judge": { + "score": 3, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": false, + "reasoning": "The summary correctly links related issues and suggests a shared root cause but fails to state the specific bug (Discord thread metadata re-injection) or key affected file, so it has useful signal but is missing core details for reliable deduplication." + }, + "usage": { + "input_tokens": 635, + "output_tokens": 632 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "Two runtime regressions affecting Discord threads: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing unbounded context growth and performance degradation, and the session resolver falling back to a global agent:*:main so CLI/session routing resolves to the wrong session.", + "solution_summary": "Manual patches were applied to compiled runtime bundles (several dist/*.js files) to fix context propagation and resolver lookup by delivery context, with an idempotent re-application script and timestamped backups created; gateway was restarted and verified, and a verification checklist completed.", + "maintainer_signal_summary": "The fixes are manual edits to compiled bundles and will be lost on updates; maintainers should upstream the changes into source/builds, keep the re-apply script and backups, and add smoke/long-thread tests and monitoring to prevent regressions.", + "dedupe_summary": "Related to existing issues about context growth in long conversations, CLI session resolution, and Discord thread handling; treat this as the runtime/patch instance of those regressions and consolidate tracking under those prior reports. " + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "The summary clearly identifies the core issues — context growth in long conversations, CLI/session resolution, and Discord thread handling as a runtime/patch instance to consolidate with prior reports — but omits some specific technical artifacts and field names present in the original which would be helpful for perfect matching." + }, + "usage": { + "input_tokens": 611, + "output_tokens": 525 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "OpenClaw receives Discord messages and the model generates replies, but sending the final reply fails with \"TypeError: fetch failed\" despite logs showing both gateway and REST proxy enabled and the proxy working for curl. Occurs in DMs and group chats on OpenClaw 2026.3.2 running under Node 22.22.0.", + "solution_summary": "No fix or patch provided in the thread. Likely causes are the HTTP fetch call for sending replies not honoring the configured proxy or a runtime/fetch compatibility issue; recommended actions are to add fetch-stack debug logs, confirm proxy usage in the REST send path, reproduce with an equivalent curl request, and provide stack traces.", + "maintainer_signal_summary": "No maintainer comments or triage activity in the thread; the issue was opened and labeled bug and regression but not acted on yet.", + "dedupe_summary": "No duplicate issues referenced; appears to be a unique report." + }, + "judge": { + "score": 1, + "has_boilerplate": false, + "signal_density": 1, + "would_cluster_correctly": false, + "reasoning": "The summary does not describe the core technical problem (Discord proxy fetch failures when sending replies) and only states uniqueness, so it provides no useful signal for deduplication." + }, + "usage": { + "input_tokens": 551, + "output_tokens": 577 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "In v2026.3.11 the CLI command openclaw agent --channel discord --to resolves to agent:*:main instead of the live Discord channel/thread session because the compiled session resolver does not look up sessions by delivery context (channel+to+accountId) and Discord targets are not normalized (e.g. \"148...\" vs \"channel:148...\").", + "solution_summary": "A manual patch was applied to dist/gateway-cli-BjsM6fWb.js to normalize Discord targets, resolve sessions by the real delivery context (channel+to+accountId), and lookup existing sessions before falling back to agent:*:main; post-patch validation shows messages route to the correct channel/thread session.", + "maintainer_signal_summary": "This is a high-priority regression that requires an upstream source-level fix and tests to persist across releases; the current hotfix is high-risk because it edits compiled files in dist and will be lost on reinstall/upgrade.", + "dedupe_summary": "Related to #41562 (Discord thread session delivery leaking to main session) and references #36401 (feature request for --session-key); likely a duplicate/should be tracked together with #41562." + }, + "judge": { + "score": 2, + "has_boilerplate": false, + "signal_density": 2, + "would_cluster_correctly": true, + "reasoning": "The summary just marks the issue as related/duplicate and briefly mentions a Discord session leak without the key technical details (CLI flag, resolver behavior, file involved), so it's too generic and low-signal for reliable deduplication though it may still link to the referenced duplicate." + }, + "usage": { + "input_tokens": 616, + "output_tokens": 856 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "In v2026.3.11 the compiled file dist/compact-1mmJ_KWL.js calls a non-existent function resolveAgentMainSessionKey, causing ReferenceError at runtime; the agent command fails and gateways can crash. This regression was introduced by the prior session-resolver patch.", + "solution_summary": "Manually edited dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...), which removes the ReferenceError and restores agent command and session behavior.", + "maintainer_signal_summary": "High priority and high risk: this is a regression in compiled/dist files that will be lost on update. Maintainters should fix the source, rebuild and publish a new release that includes this change, and ensure the fix is applied along with the earlier session-resolver fix.", + "dedupe_summary": "This is a follow-up regression to fix #3 (session resolver); fix #5 depends on fix #3 and both must be included in releases to avoid the ReferenceError." + }, + "judge": { + "score": 2, + "has_boilerplate": false, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "The summary is vague and refers to internal issue numbers/dependencies rather than the concrete regression (incorrect use of resolveAgentMainSessionKey vs buildAgentMainSessionKey in the compiled compact bundle), so it offers poor, noisy signal for deduplication." + }, + "usage": { + "input_tokens": 587, + "output_tokens": 608 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "Discord plugin's channels.discord.proxy setting is applied only to WebSocket connections, not REST API calls, causing health checks and message sends to fail with \"fetch failed\" in proxy-required environments despite curl working via the same proxy.", + "solution_summary": "Ensure the channels.discord.proxy value is used by the Discord REST HTTP client and healthcheck (route all HTTP requests through the configured proxy), add tests for proxied REST calls, and release a patch updating the plugin to use the proxy for both REST and WebSocket traffic.", + "maintainer_signal_summary": "Issue labeled bug and regression; no maintainer comments, assignment, or PR linked yet.", + "dedupe_summary": "Not marked as a duplicate and no related issues referenced; appears to be a new regression bug." + }, + "judge": { + "score": 1, + "has_boilerplate": false, + "signal_density": 1, + "would_cluster_correctly": false, + "reasoning": "This summary contains only procedural/meta info and omits the core technical issue (Discord proxy setting not applied to REST API calls), so it would not help cluster similar bugs." + }, + "usage": { + "input_tokens": 543, + "output_tokens": 552 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the compiled session resolver in dist/compact-1mmJ_KWL.js does not lookup sessions by delivery context (channel+to+accountId), so agent commands fall back to agent:*:main instead of resolving the live channel/session, causing routing and response association failures.", + "solution_summary": "A local manual patch to dist/compact-1mmJ_KWL.js adds delivery-context lookup (channel+to+accountId) in the agent session resolver and retains fallback to main; post-patch validation shows correct session resolution and consistent routing.", + "maintainer_signal_summary": "High regression risk: this is a compiled dist/ file change that will be overwritten by updates; maintainers should implement the fix in source/build, release an official patch, and prioritize preventing recurrence in future builds.", + "dedupe_summary": "Unique to v2026.3.11 and file dist/compact-1mmJ_KWL.js: session resolver missing delivery-context lookup; check for other reports in this release but treat as a single-source build artifact issue requiring source-level fix." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise and specific about the core problem (session resolver in dist/compact-1mmJ_KWL.js missing delivery-context lookup), but includes minor triage/action noise (release-uniqueness and treat-as-build-artifact guidance) and an unnecessary version mention." + }, + "usage": { + "input_tokens": 557, + "output_tokens": 486 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "The Control UI has most interface text hardcoded in English, creating usability barriers for non-English speakers (notably Chinese) across settings, buttons, agent/skill pages, channel configuration, and status messages.", + "solution_summary": "Extract all hardcoded strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, provide a language selector or auto-detect system language, and create infrastructure to add more languages later.", + "maintainer_signal_summary": "No maintainer feedback shown in the thread; the reporter asks if this is on the roadmap and invites contributors, marking the request Medium-High priority for UX/accessibility.", + "dedupe_summary": "Project docs already include zh-CN translations and the control-ui bundle contains hardcoded strings; recommend checking for existing i18n issues or PRs to avoid duplicate work before proceeding." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary captures the key facts (docs have zh-CN, control-ui has hardcoded strings) but adds a minor coordination recommendation instead of succinctly stating the core request to add i18n support and locale files." + }, + "usage": { + "input_tokens": 478, + "output_tokens": 544 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "WhatsApp channel could not apply channel-configured systemPrompt instructions at the account or group level, preventing per-account or per-group prompt customization and wildcard defaults.", + "solution_summary": "Adds systemPrompt support to WhatsApp config/schema with a resolution hierarchy (root account prompt overridden by account prompt; group prompt uses specific group then wildcard; account groups fully replace root groups). Implementation includes prompt composition (account prompt + resolved group prompt), tests, and docs updates.", + "maintainer_signal_summary": "Backwards compatible, WhatsApp-only change with no hot-reload/allowlist/mention-gating semantics altered; includes tests and docs and addresses issue #7011; PR is focused and will replace a previous broader PR.", + "dedupe_summary": "This PR supersedes the author's prior PR #40250 by providing a narrower change set; account-level groups replace root groups (no merge) and wildcard '*' semantics remain supported to avoid duplicate configs." + }, + "judge": { + "score": 2, + "has_boilerplate": true, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "Summary emphasizes PR precedence and merge behavior (including a PR number) instead of the core feature—adding WhatsApp account/group systemPrompt support—so it contains noise and would not reliably cluster with similar feature changes." + }, + "usage": { + "input_tokens": 2254, + "output_tokens": 390 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before agent processing, and group policy resolution could ignore account-scoped config so account-scoped groupAllowFrom/allowFrom fallbacks could fail, causing valid group messages to be rejected.", + "solution_summary": "Enable append processing only for WhatsApp group messages and propagate accountId when resolving WhatsApp group policy so account-scoped groupAllowFrom/allowFrom are considered; keep DM append history skipped and preserve existing inbound dedupe behavior.", + "maintainer_signal_summary": "Bugfix touching src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts; no schema or dist changes, targeted tests added for account-scoped allowlist fallback, validated against live repro; backward compatible but watch for backlog/history processing risks and account-scoped behavior changes.", + "dedupe_summary": "Inbound dedupe remains in place and DM append history continues to be skipped to avoid replay; append processing is scoped to groups to prevent duplicate or unexpected processing of historical DMs." + }, + "judge": { + "score": 3, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": false, + "reasoning": "Summmary correctly highlights preserving inbound dedupe and scoping append processing to groups, but omits the important account-scoped policy/gating change, so it may miss duplicates related to WhatsApp account-scoped policy resolution." + }, + "usage": { + "input_tokens": 1806, + "output_tokens": 572 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because resolveClientIp() returned undefined for requests from a trusted proxy that omitted forwarding headers, and isLocalDirectRequest() failed to fall back to the socket address when allowRealIpFallback was enabled and XFF resolved to a non-loopback Tailscale IP.", + "solution_summary": "resolveClientIp() now falls back to the socket remoteAddress when no forwarding headers are present; isLocalDirectRequest() checks req.socket.remoteAddress when allowRealIpFallback is enabled; isLocalishHost() gates .ts.net acceptance behind the existing allowTailscale flag and that flag is threaded through callers; docs updated and tests added.", + "maintainer_signal_summary": "All tests updated and passing; change is backward compatible and preserves existing behavior when tailscale.mode=\"serve\" because allowTailscale defaults to true; users behind Tailscale Serve should set gateway.trustedProxies and gateway.allowRealIpFallback as documented; no new auth modes introduced and this is a security hardening fix.", + "dedupe_summary": "This closes issue #28472 and depends on PR #50636 for overlapping allowTailscale gating; no other duplicate issues noted." + }, + "judge": { + "score": 1, + "has_boilerplate": true, + "signal_density": 1, + "would_cluster_correctly": false, + "reasoning": "The summary only references issue/PR numbers and dependency info and contains no technical description of the actual bugfix or changes, so it won't help cluster similar issues." + }, + "usage": { + "input_tokens": 1645, + "output_tokens": 609 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "checkInboundAccessControl computed an isSelfChat flag but never used it as a guard, so when WhatsApp channel config lacked an explicit groupPolicy non-self messages (groups, stranger DMs, broadcasts) could pass through even in self-chat deployments, violating the intended isolation boundary.", + "solution_summary": "Add an early-return guard in checkInboundAccessControl that rejects any non-self message when account.selfChatMode === true, placed before group/DM policy evaluation; the change checks the explicit config flag (not the heuristic isSelfChat) and preserves isSelfChat in results for UI/logging.", + "maintainer_signal_summary": "All 9 existing access-control tests pass; author verified code paths and scenarios, limited the change to src/web/inbound/access-control.ts, documented intentional behavior (auto-detected self-chat unchanged), and provided quick-revert instructions; some review conversations remain for maintainer judgment.", + "dedupe_summary": "This PR is the canonical fix for explicit selfChatMode enforcement; it does not affect auto-detected self-chat heuristics or other channels/policies and may be followed by a separate change to handle heuristic-only cases if desired." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise and focused on enforcing explicit selfChatMode and excluding heuristics, but it omits the platform (WhatsApp) and the specific behavior change (an early-return guard that blocks non-self messages)." + }, + "usage": { + "input_tokens": 1360, + "output_tokens": 816 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "With groupPolicy set to \"allowlist\", group messages from senders not in groupAllowFrom are dropped at access-control and never reach the mention-gating/context-storage logic, so agents cannot see non-allowlisted group messages as context even when docs describe storing them for context-only.", + "solution_summary": "Introduce an opt-in config flag groupContextFromAll (top-level and per-account, default false) that causes non-allowlisted group messages to be marked storeForContext/contextOnly and saved to pending group history (no replies or read receipts), plus related type/schema and pipeline plumbing changes.", + "maintainer_signal_summary": "PR is opt-in and backward compatible by default, locally tested and small-scope, but Greptile flags a 3/5 confidence due to potential allowlist normalization mismatches and bypassing of group gating/state updates; labels include channel: whatsapp-web, stale, size: S.", + "dedupe_summary": "Likely overlaps with prior issues about group allowlist behavior, mention-gating, and context-storage; key de-dupe targets are any open issues on allowlist/normalization or 'store group messages for context' behavior, and this PR should be reconciled with discussions about applyGroupGating side effects." + }, + "judge": { + "score": 2, + "has_boilerplate": false, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "The summary is vague and meta — it references related topics (allowlist, mention-gating, context-storage) but fails to state the core change (store non-allowlisted group messages as pending context) and includes noisy guidance about dedupe targets rather than a concise technical problem description." + }, + "usage": { + "input_tokens": 1377, + "output_tokens": 709 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "sessions_spawn previously always created a new random-UUID session for each sub-agent spawn, preventing reuse of conversation history and allowing session keys to bypass allowlist/security checks.", + "solution_summary": "Introduce an optional sessionKey param: short keys map to agent:{agentId}:subagent:{key}, fully-qualified keys are validated to match targetAgentId, and omission falls back to crypto.randomUUID(); add four tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch rejection.", + "maintainer_signal_summary": "Reviewer flagged a blocking test compile error (importing non-existent openclaw-tools.js instead of clawdbot-tools.js), a case-sensitivity bug in fully-qualified key validation (embedded agentId not normalized), and inconsistent error handling (throw vs jsonResult); confidence low (2/5) and tests/files need fixes before merge.", + "dedupe_summary": "The test import looks like a typo causing a non-compiling duplicate test path; fix the import/name to align with existing clawdbot tests to remove redundancy and restore test coverage." + }, + "judge": { + "score": 1, + "has_boilerplate": false, + "signal_density": 1, + "would_cluster_correctly": false, + "reasoning": "The summary describes a test import typo and duplicate test path unrelated to the PR's core change (adding a sessionKey for deterministic sub-agent sessions), so it would mislead clustering and fail to match similar items." + }, + "usage": { + "input_tokens": 1430, + "output_tokens": 742 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) via the generic message(edit) flow called api.editMessageText(), causing Telegram to return 400 \"there is no text in the message to edit\" and leaving edits failing and UI inconsistent.", + "solution_summary": "editMessageTelegram now catches the specific 400 error (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption for captionable media; buttons-only updates call editMessageReplyMarkup directly; HTML parse-mode fallback and MESSAGE_NOT_MODIFIED handling preserved; six unit tests added to cover the paths.", + "maintainer_signal_summary": "Labels: channel: telegram, stale, size: M. Change is limited to src/telegram/send.ts, tests pass (510/510) with 6 new tests, Greptile confidence 5/5 and safe-to-merge signal from CI and test coverage.", + "dedupe_summary": "Backward-compatible fix with no interface or config changes and no linked/closing issue; risk noted that the error-regex may break if Telegram changes wording, but failures will gracefully propagate as before; revert by restoring src/telegram/send.ts if needed." + }, + "judge": { + "score": 2, + "has_boilerplate": false, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "The summary focuses on backwards-compatibility and risk/revert notes but omits the core technical change (auto-detecting captionable messages and retrying with editMessageCaption on a specific 400), so it lacks the specific signal needed to cluster similar Telegram edit/caption bugs." + }, + "usage": { + "input_tokens": 1311, + "output_tokens": 656 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "PR #24962 hardcoded disableBlockStreaming: true for WhatsApp to block reasoning/thinking leaks, which unintentionally disabled block streaming entirely so messages were batched and only sent when the agent finished—even when per-account or global block streaming was configured.", + "solution_summary": "The deliver callback now only suppresses payloads with kind === \"tool\" while reasoning payloads are filtered upstream by shouldSuppressReasoningPayload, and disableBlockStreaming is now driven by per-account channels.whatsapp.blockStreaming with fallback to agents.defaults.blockStreamingDefault; tests and live verification show progressive delivery works and no reasoning/tool blocks leak.", + "maintainer_signal_summary": "All 13 inbound contract tests pass and live WhatsApp testing confirms progressive text_end streaming when enabled; default behavior remains off unless explicitly enabled. Quick revert options: set channels.whatsapp.blockStreaming:false or remove blockStreamingDefault; primary files touched include extensions/whatsapp/src/auto-reply/monitor/process-message.ts and dispatch-from-config.ts. Original leak fixes (#24954, #24605) remain effective.", + "dedupe_summary": "Related: PR #24962 (introduced the regression) and issues #24954, #24605 (original leaks). Any open reports about WhatsApp streaming being disabled should be considered duplicates of this fix restoring config-driven block streaming." + }, + "judge": { + "score": 3, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Identifies that the PR fixes a WhatsApp block-streaming regression and links related reports, but omits the specific technical changes (restoring config-driven disableBlockStreaming and the deliver callback behavior) that would improve clustering accuracy." + }, + "usage": { + "input_tokens": 1303, + "output_tokens": 802 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Sending videos (larger than the old 5MB default) caused the Telegram gateway to hang because content-length checks prevented response bodies from being consumed and Agent.close() deadlocked, rendering the channel unresponsive until restart.", + "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher deadlock by adding a timeout and fallback destroy, made media download failures non-fatal, added timeouts/retries for getFile and downloads, and updated stripInboundMetadata to recognize inbound media paths.", + "maintainer_signal_summary": "Labeled as a bugfix and security hardening affecting gateway, integrations, and UI; includes tests (strip-inbound-meta.test.ts 15/15) and live verification, is backward-compatible and configurable, introduces no new dependencies and preserves existing SSRF policy.", + "dedupe_summary": "Related to inbound media paths commit 49196e024; not a duplicate of SSRF or storage changes and no other duplicate PRs referenced." + }, + "judge": { + "score": 1, + "has_boilerplate": true, + "signal_density": 1, + "would_cluster_correctly": false, + "reasoning": "The summary only references a commit hash and states it's not a duplicate of SSRF/storage changes, but fails to describe the core bug fixes or behavior (Telegram gateway hang, media download limits, timeout/retry fixes), so it provides almost no useful signal for clustering." + }, + "usage": { + "input_tokens": 1399, + "output_tokens": 744 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "Telegram inbound fallback body text used a hardcoded for non-audio media when no caption/text was present, causing documents (PDF, DOCX) and other non-photo media to be misidentified and the agent to respond incorrectly.", + "solution_summary": "Change resolveTelegramInboundBody to use the already-computed type-aware placeholder (e.g. , ) instead of hardcoded , and add unit tests verifying placeholders, media detection, and getFile behavior for document messages.", + "maintainer_signal_summary": "PR marked as a bug fix, closes #7116, limited scope (only agent-facing placeholder text), no security or permission changes, tests added and passing, reviewer conversations addressed with some unresolved items needing maintainer judgment.", + "dedupe_summary": "Root cause existed since original body computation (worked for photos but not other media); no duplicate fix noted in thread; new unit tests added to prevent regressions for document/media placeholder handling." + }, + "judge": { + "score": 3, + "has_boilerplate": true, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Captures root cause and that tests were added, but omits the key detail that the inbound fallback used a hardcoded and was changed to a type-aware placeholder, and includes an irrelevant comment about the thread." + }, + "usage": { + "input_tokens": 1226, + "output_tokens": 423 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "MEDIA_MAX_BYTES was hardcoded to 5 MB in src/media/store.ts so users could not raise the staging/upload limit, causing files larger than 5 MB (common PDFs, presentations, images) to fail to stage into sandboxed agents.", + "solution_summary": "Introduced a new configurable media.maxBytes (integer, optional, max 500 MB) read at gateway boot via get/set pattern, replaced hardcoded constant, updated error messages to reflect configured limit, preserved 5 MB default and kept deprecated constant for compatibility; unit tests pass.", + "maintainer_signal_summary": "PR closes #40880, addressed all bot review threads, passed 56 unit tests, is backward compatible with no required migration, and documents risks (disk usage) and mitigations (500 MB schema cap, TTL cleanup).", + "dedupe_summary": "Change is scoped to inbound media staging only; per-channel download limits and tools.media.* limits are unchanged, so no overlap with those configs; default behavior preserved and the deprecated constant retained to avoid breaking external consumers." + }, + "judge": { + "score": 3, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": false, + "reasoning": "The summary correctly states the scope and compatibility guarantees but omits the core actionable change (adding media.maxBytes and replacing the hardcoded MEDIA_MAX_BYTES), so it has useful signal but misses a key detail for accurate clustering." + }, + "usage": { + "input_tokens": 1347, + "output_tokens": 482 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "When Telegram media download fails (due to exceeding mediaMaxMb or Telegram API limits) the handler returned early and silently dropped the entire message, so the agent never saw the file or caption and the user received no ack or feedback.", + "solution_summary": "On download failure the code now extracts file metadata from the Telegram message, injects a metadata stub into msg.text (e.g. file name/size/type and reason), and lets the message continue through the normal pipeline so the agent can handle/respond; previous automatic user warning replies were removed.", + "maintainer_signal_summary": "This is marked a bugfix for the Telegram integration (labels: channel:telegram, size:M), verified via code/trace analysis but not live runtime tested; change is backward-compatible, revertable by a single commit, and linked to issues #24854 and #23452; risk of mutating msg.text is acknowledged but limited to the prior-error path.", + "dedupe_summary": "Related to issues #24854 and #23452 addressing the same silent-drop behavior; no other duplicates called out in the thread." + }, + "judge": { + "score": 1, + "has_boilerplate": true, + "signal_density": 1, + "would_cluster_correctly": false, + "reasoning": "The summary only references related issue numbers and 'silent-drop behavior' without describing the actual fix or symptoms, so it lacks the technical signal needed for deduplication." + }, + "usage": { + "input_tokens": 1189, + "output_tokens": 751 + } + }, + { + "thread_id": 5796, + "number": 24117, + "kind": "pull_request", + "title": "feat(cli): add --session-key targeting for openclaw agent", + "summary": { + "problem_summary": "openclaw agent lacked a --session-key option, forcing callers to rely on rotating UUID sessionId, sender-derived --to, or --agent main sessions, which causes CLI-triggered deliveries to resolve to the wrong session in multi-channel/multi-agent setups and breaks callback/automation routing.", + "solution_summary": "Adds a new --session-key CLI flag, threads sessionKey through AgentCliOpts into resolveSessionKeyForRequest and gateway parameters, updates help/validation text, and keeps resolution precedence as session-key -> session-id -> to -> agent; includes tests verifying parsing and forwarding.", + "maintainer_signal_summary": "Changes are additive, backward-compatible, and well-tested (two unit tests passing locally). Implementation touched register.agent.ts and agent-via-gateway; reviewers indicated low risk and high confidence for merging.", + "dedupe_summary": "This PR addresses recurring session-targeting reports (issues #22085, #13635, #12881) by providing an explicit stable targeting path but does not claim to fully resolve those broader reports; behavior for existing callers remains unchanged." + }, + "judge": { + "score": 2, + "has_boilerplate": true, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "Summary is vague and avoids the key technical change (adding --session-key to openclaw agent), instead referencing issue numbers and a generic “stable targeting path,” so it lacks the specific terms needed to cluster similar PRs." + }, + "usage": { + "input_tokens": 1161, + "output_tokens": 460 + } + }, + { + "thread_id": 766, + "number": 11611, + "kind": "pull_request", + "title": "feat: separate group-level allowlist from sender-level command authorization", + "summary": { + "problem_summary": "With groupPolicy set to \"allowlist\", inbound group interactions are currently gated by sender phone number (groupAllowFrom), which prevents allowing entire groups while letting any participant chat, restricts slash commands separately, and doesn’t stop the bot being added to spam groups.", + "solution_summary": "If groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, access control now gates by group JID membership (allowing anyone in an approved group to chat) while keeping slash-command authorization governed by groupAllowFrom; when no groups mapping is present, the legacy sender-based filtering is preserved.", + "maintainer_signal_summary": "Automated Greptile review rates the change 4/5 and considers it safe to merge; core logic change is localized to access-control and tests were added for the new behavior, with one noted stale inline comment in the code/documentation to fix.", + "dedupe_summary": "No duplicate behavior introduced; change preserves backward compatibility and depends on PR #3326 to avoid implicitly enabling group allowlist when groupPolicy is not explicitly \"allowlist\"." + }, + "judge": { + "score": 2, + "has_boilerplate": false, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "Summary focuses on backward-compatibility and a PR dependency but omits the core change—separating group-level allowlist from sender-level command authorization—so it lacks the necessary signal to cluster related issues." + }, + "usage": { + "input_tokens": 1211, + "output_tokens": 580 + } + }, + { + "thread_id": 17924, + "number": 46425, + "kind": "pull_request", + "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", + "summary": { + "problem_summary": "Configured telegramAccounts[].mediaMaxMb was not being applied to outbound reply messages with media because loadWebMedia in the reply delivery path always used the default 100 MB cap, so operators could not enforce lower limits for replies.", + "solution_summary": "Threaded mediaMaxBytes through the Telegram outbound reply delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path, added unit tests to verify maxBytes is passed or omitted, and kept direct-send and internals unchanged.", + "maintainer_signal_summary": "Includes two new unit tests and reports 108 related tests passing across delivery, dispatch, native-commands, and bot-message suites; build and checks were verified, reviewer comments addressed, and the PR closes #46023.", + "dedupe_summary": "This is a minimal parameter-threading fix that preserves existing defaults and direct-send behavior, avoids changing media-loading internals, and also patches the parallel native-commands path to prevent duplicated bugs." + }, + "judge": { + "score": 3, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": false, + "reasoning": "Summary correctly describes a parameter-threading fix and related paths but omits the specific Telegram/mediaMaxMb outbound-reply context and is a bit generic, so it may not reliably cluster with similar media-limit Telegram issues." + }, + "usage": { + "input_tokens": 1212, + "output_tokens": 493 + } + }, + { + "thread_id": 5712, + "number": 37050, + "kind": "pull_request", + "title": "fix(telegram): add HTML parse fallback for media captions", + "summary": { + "problem_summary": "Media sends with captions lacked the HTML-parse fallback that text messages had, so captions with invalid HTML entities caused Telegram API 400 errors and the entire media send to fail.", + "solution_summary": "Wrapped the media send callback in withTelegramHtmlParseFallback so on an HTML parse error the send is retried without parse_mode using the raw caption; applies to photo, document, video, animation, audio, voice and video note sends and includes a unit test verifying the retry.", + "maintainer_signal_summary": "Small, backward-compatible bugfix touching src/telegram/send.ts; adds one unit test (total 52 passing). Labeled channel: telegram, size: S; quick revert by restoring the send.ts change.", + "dedupe_summary": "This reuses the existing text-message HTML fallback behavior (no duplicate logic) and only alters the media send inner callback; edit-message, thread fallback and caption splitting logic remain unchanged." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise statement that media sends now reuse the text-message HTML fallback and only modify the media-send callback (leaving edit/thread/caption-splitting logic alone), but it omits the explicit failure case and retry-without-parse_mode detail." + }, + "usage": { + "input_tokens": 1086, + "output_tokens": 568 + } + }, + { + "thread_id": 21769, + "number": 55280, + "kind": "pull_request", + "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", + "summary": { + "problem_summary": "WhatsApp had block streaming effectively disabled (disableBlockStreaming hardcoded true) which batched intermediate text and introduced a delivery race: block replies were enqueued optimistically and the pipeline marked sentContentKeys before confirmed delivery, causing silent turns where neither blocks nor the final reply reached the user.", + "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming, fix the boolean activation bug, add blockStreamingCoalesceDefaults, implement dispatcher.sendBlockReplyAsync and await it in onBlockReply so sentContentKeys is set only after confirmed delivery, and update tests/mocks accordingly.", + "maintainer_signal_summary": "Tests updated and passing and manual checks verify both streaming-on and off behaviors; the PR fixes the root cause (not just a workaround) and includes a usage command (openclaw config set ...) making it ready for merge.", + "dedupe_summary": "Previously dedupe suppressed final replies because the pipeline optimistically marked blocks as sent; now the pipeline waits for confirmed block delivery before marking sentContentKeys, and falls back to the final reply if block delivery fails, preventing silent turns." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core delivery-race bug and its fix (waiting for confirmed block delivery and falling back to final reply) without boilerplate, so it will cluster with similar fixes." + }, + "usage": { + "input_tokens": 999, + "output_tokens": 772 + } + }, + { + "thread_id": 8098, + "number": 25543, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked group-level allowlist support, so administrators couldn't explicitly allow specific Signal groups by ID to bypass sender-level groupAllowFrom checks or run control commands.", + "solution_summary": "Adds channels.signal.groups. config with a SignalGroupConfig type and Zod schema, wires resolveChannelGroupPolicy into the Signal event handler to apply group-level bypasses for message delivery and command authorization, and updates docs; TypeScript compiles and manual testing was performed.", + "maintainer_signal_summary": "Introduces SignalGroupConfig (requireMention, tools, toolsBySender), integrates groupExplicitlyAllowed into commandGate authorizers, and uses hasGroupAllowFrom to maintain backward compatibility; follows patterns from other channels and is marked safe to merge by the reviewer.", + "dedupe_summary": "Supersedes PR #4337 and fixes #25540 by addressing Greptile review feedback (command gating and hasGroupAllowFrom handling); no other duplicate changes indicated." + }, + "judge": { + "score": 3, + "has_boilerplate": true, + "signal_density": 3, + "would_cluster_correctly": false, + "reasoning": "Mentions fixing command gating and hasGroupAllowFrom and references PR/issue numbers, but omits the key 'Signal' and the new groups config detail and includes PR/issue boilerplate, so it has some signal but misses important specifics for reliable clustering." + }, + "usage": { + "input_tokens": 941, + "output_tokens": 565 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal lacked group-level allowlisting via channels.signal.groups, so when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter and never matches group IDs.", + "solution_summary": "Added a groups config to SignalAccountSchemaBase, invoked resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom, and added four tests; all relevant test suites pass.", + "maintainer_signal_summary": "Small-size PR labeled channel: signal and fixes #25540. Fully tested: 624 config tests, 6 Signal monitor tests, and 17 group-policy tests (including 4 new) all pass. PR was AI-assisted with human oversight by JamesPeck.", + "dedupe_summary": "This change brings Signal into parity with other channels that already support a groups config (Telegram, WhatsApp, iMessage, IRC, BlueBubbles); it fixes a unique missing call in Signal rather than duplicating an existing fix." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise summary that Signal now supports a 'groups' config like other channels and fixes the missing group-level allowlist path, but it’s slightly vague about the exact schema/call added." + }, + "usage": { + "input_tokens": 741, + "output_tokens": 643 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "WhatsApp messages are delivered as a single batch because process-message.ts hardcodes disableBlockStreaming:true, so intermediate blocks and tool-call/sleep-delimited messages are not sent separately; there was also a bug treating queuedFinal as the only delivered marker.", + "solution_summary": "Respect channels.whatsapp.blockStreaming by adding resolveWhatsAppBlockStreamingEnabled and replacing the hardcoded flag, fix queuedFinal logic so intermediate block payloads count as replies, register blockStreaming capability and default in the WhatsApp dock, and update tests to cover enabled/disabled behavior.", + "maintainer_signal_summary": "Tests were updated and all 10 existing tests pass; tests now mock counts like other channels and verify anyReplyDelivered for intermediate blocks. Change is backward-compatible by default (blockStreaming false) and labeled channel: whatsapp-web, size: S.", + "dedupe_summary": "This change aligns WhatsApp with existing Telegram/Line blockStreaming behavior to avoid duplicate feature-requests; if blockStreaming is omitted it remains backward-compatible, so duplicate issues should reference this PR and the shared blockStreaming config." + }, + "judge": { + "score": 3, + "has_boilerplate": true, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Summarizes the core change (WhatsApp adopting Telegram/Line blockStreaming behavior and remaining backward-compatible) but includes meta/process noise about duplicate feature-requests and omits the important technical fix around queuedFinal/intermediate payload handling." + }, + "usage": { + "input_tokens": 491, + "output_tokens": 618 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v1-strip-boilerplate.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v1-strip-boilerplate.json new file mode 100644 index 0000000..baf6af8 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v1-strip-boilerplate.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v1-strip-boilerplate", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "Summarize this GitHub issue or pull request. Your summary will be embedded as a vector and used for duplicate/similar-issue detection via clustering.\n\nIGNORE and strip away:\n- Issue/PR template sections (### Bug type, ### Steps to reproduce, ### Expected behavior, ### Checklist, etc.)\n- Testing instructions, QA notes, deployment steps\n- Checkbox lists and checklists\n- Version numbers, OS details, and environment info (unless they ARE the bug)\n- \"Related issues\" or \"References\" sections\n- Auto-generated content\n\nFOCUS on extracting:\n- The core problem or feature being described\n- Specific technical components, APIs, or subsystems affected\n- The root cause or proposed solution (if stated)\n\nReturn concise JSON with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field: plain text, no markdown, 1-3 sentences. The dedupe_summary should capture the essential topic in a way that would match other reports of the same issue.", + "timestamp": "2026-03-30T15:58:04.012Z", + "aggregate": { + "avg_score": 4.95, + "avg_signal_density": 4.95, + "boilerplate_count": 0, + "boilerplate_pct": 0, + "would_cluster_correctly_pct": 100, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 46623, + "total_output_tokens": 24277 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "channels.discord.proxy only applies to the Discord WebSocket Gateway connection; REST API calls during startup (deploy commands, fetch bot identity) and guild resolution do not use the configured HTTP proxy, causing fetch timeouts and guild channels to be non-functional behind a proxy while DMs still work.", + "solution_summary": "Ensure channels.discord.proxy (or a global proxy setting) configures an undici ProxyAgent or setGlobalDispatcher for all Discord REST requests and the guild-resolve code path during provider startup; consider adding retry logic for guild resolve.", + "maintainer_signal_summary": "Regression with reproduction steps and logs included: WebSocket connects via proxy but REST calls fail with \"fetch failed\" after ~10s. Workaround using NODE_OPTIONS to set a ProxyAgent fixes REST but guild resolve remains unresolved; standalone CLI resolve works when ProxyAgent is applied. Affected in proxy-required environments; suggested fix is to propagate proxy to undici dispatcher for all Discord HTTP calls.", + "dedupe_summary": "Discord channel proxy only proxies Gateway websocket, not REST API or guild resolve, causing startup REST failures and non-functional guild channels behind HTTP proxies." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that clearly states the core bug (proxying only the Gateway websocket, not REST/guild resolve) and the impact (startup REST failures and broken guild channels behind HTTP proxies)." + }, + "usage": { + "input_tokens": 2301, + "output_tokens": 491 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "Outbound Discord REST requests (sending messages/attachments) fail with \"fetch failed\" while the gateway WebSocket works via proxy. The channels.discord.proxy config is not applied to REST sends because the Carbon RequestClient uses fetch without proxy support, so REST calls bypass the configured proxy in restricted networks.", + "solution_summary": "Modify the Carbon RequestClient or its fetch usage to support proxies and honor channels.discord.proxy (e.g., use proxy-aware HTTP client or add HTTP/SOCKS/TUN proxy agent) so all REST requests, uploads and downloads route through the configured proxy.", + "maintainer_signal_summary": "User in a restricted region reproduces the issue with channels.discord.proxy set to http://127.0.0.1:7890/; logs show REST proxy enabled but outgoing REST requests still fail with TypeError: fetch failed. This prevents bots from sending messages and is high-impact for proxied environments.", + "dedupe_summary": "channels.discord.proxy not applied to Discord REST API calls — Carbon RequestClient/fetch lacks proxy support, causing outgoing message/attachment sends to fail behind proxies." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary stating that channels.discord.proxy isn't applied to REST calls because the Carbon RequestClient/fetch lacks proxy support, which directly explains the outgoing-send failures behind a proxy." + }, + "usage": { + "input_tokens": 983, + "output_tokens": 659 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "When OpenClaw is configured with an HTTP proxy for Discord, the Discord provider can log in and open the gateway but some startup REST calls (native command deployment and bot identity fetch) consistently fail with \"fetch failed\" while other Discord requests (application ID fetch, gateway) succeed.", + "solution_summary": "Investigate and fix inconsistent proxy usage in the Discord provider so all REST calls use the same proxy-aware fetch/dispatcher as the gateway/login path; update transport or dispatcher code paths to ensure proxy handling is applied uniformly for startup REST endpoints.", + "maintainer_signal_summary": "User provided logs, environment details (proxy configured via channels.discord.proxy), and repeated reproducible failures across restarts; offered to provide more environment details or test patches. The issue appears specific to OpenClaw's Discord provider rather than token/config errors.", + "dedupe_summary": "Discord provider behind HTTP proxy: gateway connects but some REST startup calls (deploy commands, fetch bot identity) fail with 'fetch failed' due to inconsistent proxy handling." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core failure (Discord gateway connects but REST startup calls like command deploy and bot identity fetch fail behind an HTTP proxy) without boilerplate or noise." + }, + "usage": { + "input_tokens": 1129, + "output_tokens": 482 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "OpenClaw logs show Discord proxy as enabled but all Discord REST calls fail with \"fetch failed\" and the gateway websocket never establishes under WSL2 using an HTTP proxy, despite curl via the same proxy succeeding. The Discord channel is unusable: REST deploy and identity fetches fail and gatewayConnected remains false.", + "solution_summary": "Ensure the configured HTTP proxy is actually applied to the Discord.js REST/fetch client and gateway (e.g., pass a proxy/agent to discord.js or honor HTTP(S)_PROXY environment variables) so REST requests and WebSocket connections are routed through the proxy.", + "maintainer_signal_summary": "Reproducible 100% under WSL2 with provided config (~/.openclaw/openclaw.json) and logs showing \"discord: rest proxy enabled\" but subsequent fetch failures; Feishu channel works and workaround is TUN-mode transparent proxy. Severity: high; affects any environment requiring an HTTP proxy for Discord.", + "dedupe_summary": "Discord proxy logged as enabled but discord.js REST fetch fails (fetch failed) and gateway websocket never connects under WSL2 — proxy not actually applied to HTTP client." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary captures the core bug that the Discord proxy is logged as enabled but REST requests fail because the HTTP client isn't using the proxy; it adds a possibly extraneous claim about the gateway websocket not connecting, so minor noise." + }, + "usage": { + "input_tokens": 966, + "output_tokens": 706 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "OpenClaw's Discord channel ignores the configured HTTP(S) proxy for REST API calls: incoming WebSocket messages work, but attempts to send messages fail with TypeError: fetch failed despite proxy set in channels.discord.proxy and HTTP(S)_PROXY env vars.", + "solution_summary": "Route Discord REST API calls through the same proxy-aware SSRF guard/shared fetch path used for web tools (web_search/web_fetch) so HTTP_PROXY/HTTPS_PROXY are respected by Discord REST requests.", + "maintainer_signal_summary": "Logs show \"discord final reply failed: TypeError: fetch failed\" while curl via the same proxy to the Discord API succeeds; Telegram and Discord WebSocket respect the proxy; changelog notes a proxy fix for web tools that did not include Discord REST calls.", + "dedupe_summary": "OpenClaw Discord REST API ignores configured HTTP(S) proxy, causing fetch failed errors when sending messages." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: states that OpenClaw's Discord REST client ignores the configured HTTP(S) proxy and results in fetch failures when sending messages, with no template noise." + }, + "usage": { + "input_tokens": 989, + "output_tokens": 592 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "The CLI command openclaw agent --channel discord --to resolves deliveries to agent:*:main instead of the live Discord channel/thread session because the session resolver does not search by delivery context (channel + to + accountId) and Discord targets are not normalized.", + "solution_summary": "A manual patch to compiled dist files added delivery-context-based session resolution, normalized Discord targets, fixed lookup logic in the compact resolver, and made the command path pass channel and replyAccount/accountId; recommended fix is to implement these changes in source and add revalidation tests.", + "maintainer_signal_summary": "High-regression-risk issue because the fix was applied to compiled dist files and will be lost on updates; maintainers should port the patch to source, add tests to cover delivery-context session lookup, and revalidate post-update behavior.", + "dedupe_summary": "openclaw CLI routes Discord targets to agent:*:main instead of resolving session by delivery context (channel+to+accountId)" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug—CLI routes Discord deliveries to agent:*:main instead of resolving sessions by delivery context (channel+to+accountId) with no extraneous boilerplate." + }, + "usage": { + "input_tokens": 916, + "output_tokens": 504 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions: (1) in Discord threads, volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) was being re-injected every turn causing progressive context bloat and degradation; (2) the CLI agent command resolver routed Discord deliveries to the default agent:*:main session instead of the live thread-bound session because it didn’t resolve by delivery context (channel+to+accountId). Affected runtime bundles and the gateway CLI session resolver.", + "solution_summary": "Patched compiled runtime bundles to stop injecting the volatile Discord metadata every turn, added session resolution by normalized delivery target in the gateway CLI, added delivery-context lookup in the agent command resolver, and passed channel and replyAccount/accountId into the resolver; also fixed an incorrect resolveAgentMainSessionKey usage. Verified by restarting the gateway service and observing correct writer/reasoning sessions and no repeated context.", + "maintainer_signal_summary": "Fixes were applied directly to compiled bundles in openclaw/dist/ (dist/pi-embedded-…, dist/gateway-cli-…, dist/compact-…, dist/register.agent-…), so changes are manual and will be lost on reinstall; maintainers should reapply or add a persistent patch/script post-update. Service restart confirmed resolution.", + "dedupe_summary": "Discord thread handling: volatile metadata re-injected every turn causing context bloat and CLI/gateway routing not resolving sessions by delivery context, causing commands to land in main instead of thread session." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, accurate summary that captures both core regressions (volatile metadata re-injection causing context bloat and incorrect session resolution for CLI commands) with no template noise, so it will cluster properly with similar issues." + }, + "usage": { + "input_tokens": 835, + "output_tokens": 591 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "The sandbox media staging cap is hardcoded to 5 MB in dist/store-*.js (const MEDIA_MAX_BYTES = 5 * 1024 * 1024), causing files larger than 5 MB (PDFs, PPTX, images) to be rejected or silently fail when copied into a sandboxed agent.", + "solution_summary": "Make the MEDIA_MAX_BYTES value configurable (for example via openclaw.json like tools.media.maxBytes) or otherwise derive it from existing per-type limits so the staging cap can be increased/overridden instead of being hardcoded.", + "maintainer_signal_summary": "Reporter marks this a regression present since at least v2026.3.7 and still in v2026.3.8, provides a manual sed patch workaround to alter dist/store-*.js, and requests adding configuration support for the staging cap.", + "dedupe_summary": "Hardcoded 5 MB sandbox media staging limit prevents uploading files >5MB; add tools.media.maxBytes config to override MEDIA_MAX_BYTES." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and technical: identifies the hardcoded MEDIA_MAX_BYTES 5MB sandbox staging cap and the desired tools.media.maxBytes config override without extraneous boilerplate." + }, + "usage": { + "input_tokens": 882, + "output_tokens": 617 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "Discord provider cannot send REST API requests: every REST call fails with a TypeError 'fetch failed' even though the Discord rest proxy is configured and logs show 'rest proxy enabled'. The Gateway WebSocket works and bot receives messages, and curl requests through the same proxy succeed, so only OpenClaw's Discord REST interactions are broken.", + "solution_summary": "No solution or workaround is provided in the report; the user supplied configuration and logs but did not identify a root cause. The issue appears to be a bug in OpenClaw's Discord REST/proxy integration (root cause not determined).", + "maintainer_signal_summary": "Reporter provided config, gateway logs, and reproduction evidence (curl works, Telegram unaffected) and requests a fix; severity unspecified but reproducible for Discord REST operations. Logs show 'fetch failed' errors during command deployment and identity fetch, indicating the REST path is failing despite proxy being enabled.", + "dedupe_summary": "Discord REST API requests in OpenClaw fail with 'fetch failed' when using a configured proxy, while Gateway WebSocket works and direct curl requests via the same proxy succeed." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary that captures the core bug (Discord REST requests in OpenClaw failing with 'fetch failed' despite proxy configuration, while WebSocket and curl via the same proxy work), which is the right signal for deduplication." + }, + "usage": { + "input_tokens": 827, + "output_tokens": 693 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "Discord REST API calls made by @buape/carbon's RequestClient ignore the channels.discord.proxy setting in openclaw.json, so REST requests (and related fetches like memorySearch and attachment downloads) bypass the configured proxy while the WebSocket gateway respects it. The RequestClient calls globalThis.fetch without a proxy-aware dispatcher and Node.js native fetch does not use HTTP_PROXY/HTTPS_PROXY unless special flags or a patched dispatcher are used.", + "solution_summary": "Have @buape/carbon's REST client accept an optional dispatcher or proxy configuration and have OpenClaw pass channels.discord.proxy to it so REST calls use the same proxy as the WebSocket. Short-term workarounds include setting a global undici dispatcher via a preload script (EnvHttpProxyAgent) rather than using NODE_OPTIONS=--use-env-proxy which breaks WebSockets.", + "maintainer_signal_summary": "Reporter asks maintainers to update the carbon RequestClient to accept and use a dispatcher/proxy and for OpenClaw to forward channels.discord.proxy. They provide a safe workaround (preloading a global EnvHttpProxyAgent) and warn that using --use-env-proxy breaks WebSocket connections.", + "dedupe_summary": "REST API requests via @buape/carbon ignore channels.discord.proxy because RequestClient uses globalThis.fetch without a proxy dispatcher, causing REST calls and media fetches to bypass configured proxy while WebSocket uses it." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core bug: RequestClient uses globalThis.fetch and ignores channels.discord.proxy so REST/media requests bypass the configured proxy while the gateway uses it." + }, + "usage": { + "input_tokens": 836, + "output_tokens": 582 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "Under sustained load with long-running conversations OpenClaw suffers performance degradation from unbounded context token growth (writer sessions ~42k tokens, reasoning ~90k) that increases per-turn latency, Discord lane blocking that causes serialization under load, and typing indicators getting stuck until TTL. Root causes identified are lack of effective context token limits, overly high concurrency saturating lanes, and insufficient event queue timeouts.", + "solution_summary": "Applied configuration changes to reduce concurrency (maxConcurrentSessions, agents.defaults.maxConcurrent, subagents limits), set agents.defaults.contextTokens to 80000, and increased channels.discord.eventQueue.listenerTimeout; restarted the gateway and verified no new \"lane wait exceeded\" or \"typing TTL reached\" errors. Also improved an anti-regression patching script to cover multiple hash variants and recommended making limits adaptive or configurable via openclaw config.", + "maintainer_signal_summary": "Maintainers lowered concurrency limits, added a context token cap, extended the Discord event listener timeout, restarted the gateway, and confirmed absence of new lane/typing errors post-restart, while noting restarts alone don’t fully eliminate cases under sustained load. They also hardened the patching script and suggested exposing/adapting limits rather than manual JSON edits.", + "dedupe_summary": "Performance degradation caused by unbounded context token growth and lane blocking under load, mitigated by capping context tokens, reducing concurrency, and increasing event queue timeouts." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core failure modes (unbounded context growth and lane blocking) and the applied mitigations, with no boilerplate or extraneous details, so it will cluster with similar reports." + }, + "usage": { + "input_tokens": 770, + "output_tokens": 698 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every conversation turn, causing context contamination, increased token usage, prompt cache busting, and performance degradation. The compiled runtime treats Discord like other platforms but lacks an isNewSession guard to limit injection to the first turn.", + "solution_summary": "Manual patches were applied to compiled files: dist/pi-embedded-D6PpOsxP.js to stop injecting the listed fields, and dist/compact-1mmJ_KWL.js to add an isNewSession guard and replace resolveAgentMainSessionKey with buildAgentMainSessionKey. Recommended follow-up is to apply the fix in source code and add a post-update revalidation script.", + "maintainer_signal_summary": "High regression risk because changes were made directly to compiled dist files and will be lost on update; local validation shows restored toolCall sessions and resolved prompt caching issues, but an official source-level patch is required and should be prioritized.", + "dedupe_summary": "Discord threads re-inject volatile metadata each turn (message_id, reply_to_id, timestamp, Thread starter, history), contaminating context and busting prompt cache." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core bug—Discord thread metadata is repeatedly injected each turn (fields listed), causing context pollution and prompt cache loss, with no extraneous boilerplate." + }, + "usage": { + "input_tokens": 782, + "output_tokens": 741 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord thread messages have volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) re-injected on every turn, causing context pollution, token bloat, degraded performance and prompt cache busting. The compiled runtime in dist/pi-embedded-D6PpOsxP.js is treating Discord threads as needing repeated volatile metadata, unlike Slack/iMessage fixes.", + "solution_summary": "A manual patch was applied to dist/pi-embedded-D6PpOsxP.js to avoid injecting those metadata fields after the first turn, restoring prompt caching and reducing token usage; recommended next steps are to implement the fix in source code and add a post-update revalidation script to avoid regression.", + "maintainer_signal_summary": "High regression risk because the change was made to compiled files in dist and will be lost on reinstall; requires an official source-level fix, tests/CI to prevent recurrence, and attention to related issues (#41355, #34228, #37047).", + "dedupe_summary": "Discord thread metadata re-injected every message (message_id, reply_to_id, timestamp, thread starter, chat history) causing context pollution and prompt cache busting due to a bug in compiled runtime dist/pi-embedded-D6PpOsxP.js." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that names the exact metadata re-injected, the impact (context pollution and cache busting), and the compiled runtime file causing it, with no boilerplate or noise." + }, + "usage": { + "input_tokens": 790, + "output_tokens": 809 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "Two regressions in Discord thread handling: repeated per-turn injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing unbounded context growth and latency in long threads, and the session resolver falling back to agent:*:main (wrong session) because CLI/resolver lookup did not consistently use delivery context (channel + to + accountId) and command path lacked sufficient context. The root cause was missing runtime patches in compiled dist/*.js bundles.", + "solution_summary": "Applied manual runtime patches to the affected dist/*.js bundles, restarted the gateway, created automatic backups and an idempotent re-application script, and validated that thread-bound sessions now resolve correctly; recommended non-regression tests (long-thread conversations, 15–30 minute monitoring, smoke tests for latency and context size).", + "maintainer_signal_summary": "Gateway restarted and is active; no new errors in logs post-restart; thread-bound sessions resolve correctly and CLI routing now targets the correct session. Note: fixes are manual edits to compiled bundles and must be re-applied after updates using the provided script and backups.", + "dedupe_summary": "Discord threads: per-turn metadata duplication grows context and causes latency, plus session resolver falls back to agent:*:main; patched compiled runtime bundles and added reapply script." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary that captures both core bugs (per-turn metadata growth causing context/latency and session resolver fallback to agent:*:main) and notes the runtime bundle patches and reapply script without extraneous boilerplate." + }, + "usage": { + "input_tokens": 766, + "output_tokens": 729 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "OpenClaw receives Discord messages and generates responses but fails to send replies; logs show \"final reply failed: TypeError: fetch failed\" even though Discord proxy is configured and reported as enabled.", + "solution_summary": "Likely a proxy-handling bug where the configured HTTP proxy is recognized for gateway/rest but not applied to the fetch used for sending reply messages; fix would ensure Discord REST fetch calls respect the configured proxy.", + "maintainer_signal_summary": "Regression that previously worked, reproducible in DM and group chats; logs show rest and gateway proxy enabled and a fetch failure, while curl through the same proxy succeeds, indicating the issue is in OpenClaw's proxy usage for reply sending.", + "dedupe_summary": "Discord replies fail with \"fetch failed\" because configured proxy is not being applied to message-sending HTTP requests." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (Discord message send fails with 'fetch failed' due to configured proxy not being applied) and contains no template noise." + }, + "usage": { + "input_tokens": 706, + "output_tokens": 754 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "In v2026.3.11 the CLI command openclaw agent --channel discord --to ... routes messages to the fallback session agent:*:main instead of the live Discord channel/thread session because the compiled session resolver in dist/gateway-cli-BjsM6fWb.js does not lookup sessions by delivery context and Discord targets are not normalized.", + "solution_summary": "A local patch to dist/gateway-cli-BjsM6fWb.js normalizes Discord targets (e.g. 148... → channel:148...), performs session lookup by delivery context (channel+to+accountId) before falling back to main, and thereby restores correct CLI routing to the live session.", + "maintainer_signal_summary": "High regression risk: this is a manual change to compiled files in dist/ and will be lost on updates; requires a source-level fix and tests to normalize targets and add delivery-context session lookup. Related requests: #36401 and #41562.", + "dedupe_summary": "CLI routing to Discord channels resolves to agent:*:main because session resolver omits delivery-context lookup and Discord target normalization; needs session lookup by channel+to+accountId." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core bug: CLI routes to agent:*:main because the session resolver lacks delivery-context lookup and Discord target normalization, and calls for lookup by channel+to+accountId." + }, + "usage": { + "input_tokens": 771, + "output_tokens": 721 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "After applying an earlier session-resolver patch in v2026.3.11, the compiled file dist/compact-1mmJ_KWL.js calls a non-existent function resolveAgentMainSessionKey, causing ReferenceError at runtime, agent command failures, and potential gateway crashes when processing agent commands.", + "solution_summary": "Locally patched dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with the correct buildAgentMainSessionKey(...), which removes the ReferenceError and restores agent command and session behavior; note this is a manual change to compiled output and will be lost on reinstall/update.", + "maintainer_signal_summary": "High-risk regression introduced by a prior fix; maintainers should apply the correct change in source/build (not just dist) and ensure fix #5 is included whenever fix #3 (session resolver in compact) is applied to avoid ReferenceError.", + "dedupe_summary": "Regression in dist/compact-1mmJ_KWL.js: ReferenceError due to resolveAgentMainSessionKey used instead of buildAgentMainSessionKey, causing agent command failures." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: names the compiled file, the incorrect vs correct function, and the resulting ReferenceError/agent failures, with no boilerplate—will cluster well with similar regressions." + }, + "usage": { + "input_tokens": 742, + "output_tokens": 485 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "Discord channel plugin fails health checks and message sends in proxy-required environments because channels.discord.proxy is only applied to WebSocket connections; REST API (HTTP) calls bypass the proxy and return \"fetch failed\" while curl with the same proxy succeeds.", + "solution_summary": "Apply channels.discord.proxy to the plugin's HTTP/REST client and health-check requests (or make the HTTP client respect the configured proxy), so REST API calls are routed through the proxy like the WebSocket connections.", + "maintainer_signal_summary": "User reports a regression (worked previously) on OpenClaw 2026.3.8 with reproduction steps and confirms the proxy itself works via curl; this impacts CLI message sending and health checks and needs a code change in the discord channel implementation.", + "dedupe_summary": "Discord channel REST API calls ignore channels.discord.proxy causing fetch failed, failing message send and health checks." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and focused on the core bug—Discord REST API calls ignore channels.discord.proxy causing fetch failures and related message/health-check errors—so it will cluster with similar reports." + }, + "usage": { + "input_tokens": 698, + "output_tokens": 497 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the session resolver in the agent command inside the compiled bundle dist/compact-1mmJ_KWL.js does not look up sessions by delivery context (channel + to + accountId), causing resolution to fall back to agent:*:main and agent responses to be associated with the wrong session.", + "solution_summary": "User patched dist/compact-1mmJ_KWL.js to add a delivery-context lookup: search for an existing session by channel+to+accountId and only fallback to main when no specific session is found; validation shows correct routing after the change.", + "maintainer_signal_summary": "The fix was applied manually to a compiled dist file and carries HIGH regression risk because it will be lost on reinstall/update; the lookup must be implemented in the source/build pipeline and shipped in an upstream release.", + "dedupe_summary": "Compiled bundle (dist/compact-1mmJ_KWL.js) session resolver missing delivery-context (channel+to+accountId) lookup, causing fallback to agent:*:main and incorrect session routing." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies the compiled file, the missing delivery-context (channel+to+accountId) lookup, and the resulting fallback to agent:*:main causing incorrect session routing." + }, + "usage": { + "input_tokens": 712, + "output_tokens": 678 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English (settings, labels, buttons, agent/skill pages, channel configs, status messages, tooltips), creating usability barriers for non-English users; hardcoded strings are present in the distributed control-ui bundle.", + "solution_summary": "Extract UI strings into a structured i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, provide a language selector or auto-detect system language, and build infrastructure to easily add more languages.", + "maintainer_signal_summary": "Requester flags this as medium-high priority for UX accessibility, notes docs already have zh-CN translations, and asks whether i18n for control-ui is on the roadmap or if maintainers/others want to collaborate on implementation.", + "dedupe_summary": "Control UI lacks localization; request to add i18n support and a Simplified Chinese (zh-CN) locale by extracting hardcoded English strings and adding language selection/auto-detect." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary that captures the core request to add i18n support and a zh-CN locale by extracting hardcoded English strings and adding language selection/auto-detect, with no extraneous boilerplate." + }, + "usage": { + "input_tokens": 633, + "output_tokens": 565 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "WhatsApp group chats could not apply channel-level systemPrompt instructions at account or group granularity, preventing per-account or per-group customization and wildcard defaults for groups without their own prompt.", + "solution_summary": "Adds systemPrompt support to WhatsApp config types/schema and implements resolution and composition logic so the final injected prompt is account systemPrompt plus the resolved group systemPrompt (specific group then '*' wildcard). Account-level groups fully override root groups; includes tests and docs.", + "maintainer_signal_summary": "Feature PR with tests and documentation; backward compatible and limited to the WhatsApp channel only (no hot-reload, allowlist, or mention-gating changes). Closes the linked issue and replaces a previous broader PR.", + "dedupe_summary": "Add per-account and per-group systemPrompt support for WhatsApp with precedence: account prompt then group prompt (specific -> wildcard); account groups override root groups." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that states the feature (per-account and per-group WhatsApp systemPrompt) and its precedence rules, matching the core change without boilerplate." + }, + "usage": { + "input_tokens": 2409, + "output_tokens": 545 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before inbound/agent processing, and group policy helpers resolved WhatsApp group policy without accountId so account-scoped groupAllowFrom/allowFrom were ignored, causing allowlist fallback to fail closed.", + "solution_summary": "Enable append processing only for WhatsApp group messages and propagate accountId when resolving WhatsApp group policy so account-scoped groupAllowFrom/allowFrom (and allowlist fallback) are applied; DM append history remains skipped.", + "maintainer_signal_summary": "Bugfix touching web inbound and group activation code (notably src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts), validated against a live OpenClaw WhatsApp deployment with trace logs and targeted tests; change is backward-compatible and scoped to WhatsApp groups.", + "dedupe_summary": "WhatsApp group append messages skipped and account-scoped group policy ignored, preventing group messages from triggering the agent." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it names the WhatsApp group 'append' handling being skipped and the account-scoped group policy being ignored, which are the core issues needed for correct deduplication." + }, + "usage": { + "input_tokens": 1961, + "output_tokens": 712 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because the gateway failed to resolve the client IP when a trusted proxy sent requests without forwarding headers and the local-request check did not fall back to the socket remote address when allowRealIpFallback was enabled. Additionally, unconditional acceptance of .ts.net Host headers posed a security concern.", + "solution_summary": "resolveClientIp now returns the socket address as a fallback when no forwarding headers are present; isLocalDirectRequest checks req.socket.remoteAddress when allowRealIpFallback is enabled; isLocalishHost now gates .ts.net acceptance behind the existing allowTailscale flag and that flag is threaded through relevant callers. Documentation updated and tests added to verify behavior.", + "maintainer_signal_summary": "New tests added (105 passing) and failing logs fixed; PR closes issue #28472 and depends on another PR for overlapping allowTailscale changes. No new config keys introduced, but docs require gateway.trustedProxies and gateway.allowRealIpFallback when using Tailscale Serve; allowTailscale still defaults to true for tailscale.mode=\"serve\" to preserve compatibility.", + "dedupe_summary": "401 authentication failure for Canvas behind Tailscale Serve due to missing client-IP fallback and local-request detection; fixed by falling back to socket.remoteAddress and gating .ts.net host acceptance behind allowTailscale." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core bug (401 for Canvas behind Tailscale Serve), root causes (missing client-IP/socket fallback and local-request detection), and the precise fixes (fallback to socket.remoteAddress and gating .ts.net via allowTailscale), so it will cluster correctly with similar reports." + }, + "usage": { + "input_tokens": 1800, + "output_tokens": 805 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "checkInboundAccessControl computes an isSelfChat value but never uses it as a guard, so non-self WhatsApp messages (groups, DMs from strangers, broadcasts) can pass through because resolveWhatsAppRuntimeGroupPolicy defaults to \"open\" when cfg.channels.whatsapp exists with no explicit groupPolicy. This violates the intended self-chat isolation boundary and lets non-self messages reach downstream processing.", + "solution_summary": "Add an early-return guard in checkInboundAccessControl that rejects any non-self message when account.selfChatMode === true, placed before group/DM policy evaluation. The guard checks the explicit config flag only (not the heuristic isSelfChat) to avoid blocking teammates allowed via allowFrom; isSelfChat remains computed for UI/logging.", + "maintainer_signal_summary": "Change limited to src/web/inbound/access-control.ts with tests updated/verified; all 9 related tests pass and code-path analysis confirms the guard fires before policy resolution. The fix is intentionally scoped to explicit selfChatMode and can be reverted by removing the early-return guard; reviewers should note auto-detected self-chat is not hard-blocked by design.", + "dedupe_summary": "WhatsApp self-chat mode not enforcing isolation because isSelfChat was computed but unused, allowing group/other non-self messages through; fixed by enforcing explicit selfChatMode guard in inbound access control." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the bug (isSelfChat computed but unused), its effect (non-self WhatsApp messages bypass isolation), and the fix (explicit selfChatMode guard in inbound access control)." + }, + "usage": { + "input_tokens": 1515, + "output_tokens": 583 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "When groupPolicy is set to \"allowlist\", group messages from senders not in groupAllowFrom are dropped at access-control and never reach the mention-gating/context-storage logic, so agents cannot see those messages in the \"Chat messages since your last reply\" context block.", + "solution_summary": "Adds an opt-in flag groupContextFromAll (top-level and per-account, default false). When enabled, access-control returns storeForContext for non-allowlisted senders, messages are marked contextOnly (no read receipts), stored as pending group history, and do not trigger replies; per-account config takes precedence.", + "maintainer_signal_summary": "PR is localized and opt-in with zero default behavior change and local verification with two bots; greptile flagged a 3/5 confidence due to potential allowlist normalization/matching bugs and the new context-only path bypassing existing group gating/state updates that may cause subtle inconsistencies.", + "dedupe_summary": "WhatsApp groups: non-allowlisted senders' messages are currently dropped under groupPolicy: allowlist; add groupContextFromAll flag to store those messages as contextOnly instead of discarding." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: states that with groupPolicy: allowlist non-allowlisted group messages are dropped and the change adds a groupContextFromAll flag to store them as context-only instead of discarding." + }, + "usage": { + "input_tokens": 1532, + "output_tokens": 562 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "Adds an optional sessionKey parameter to sessions_spawn so sub-agents can reuse deterministic sessions instead of creating a new random-UUID each spawn. Short keys are namespaced to agent:{agentId}:subagent:{key}, fully-qualified keys are accepted only if the embedded agentId matches the target agent, and omission falls back to crypto.randomUUID(). Also intended to fix a session key allowlist bypass and includes four unit tests for deterministic and fallback behaviors.", + "solution_summary": "Implements sessionKey handling, namespacing, and cross-agent validation in sessions_spawn, with a fallback to crypto.randomUUID() when sessionKey is omitted; adds tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch rejection. Note: tests currently reference a non-existent import which prevents compilation.", + "maintainer_signal_summary": "Automated review flagged blocking issues: a test imports ./openclaw-tools.js which does not exist (should be ./clawdbot-tools.js), a case-sensitivity bug where embedded agentId from fully-qualified keys isn’t normalized against normalizeAgentId(), and inconsistent error handling (throw vs returning jsonResult) for cross-agent mismatches; these must be fixed before merging.", + "dedupe_summary": "Add sessionKey to sessions_spawn to reuse deterministic sub-agent sessions with namespacing and cross-agent validation, plus tests (but test import, case-sensitivity, and error-handling issues reported)." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summarizes the core change—adding a deterministic sessionKey to sessions_spawn with namespacing and cross-agent validation—so it will cluster correctly, but it includes noisy test/import/case-sensitivity/error-handling remarks that are not essential for deduplication." + }, + "usage": { + "input_tokens": 1585, + "output_tokens": 677 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Editing Telegram messages with media (photo, video, audio, document, animation) using the generic edit path calls editMessageText and gets a 400 error \"there is no text in the message to edit\", causing message(action=edit) to fail for captionable messages and leave the UI inconsistent.", + "solution_summary": "editMessageTelegram in src/telegram/send.ts now detects that specific 400 error via NO_TEXT_IN_MESSAGE_RE, retries with editMessageCaption for captionable messages, and uses editMessageReplyMarkup directly for buttons-only updates; MESSAGE_NOT_MODIFIED is treated as success and HTML parse fallback is preserved.", + "maintainer_signal_summary": "Six unit tests added covering retry path, buttons-only path, error propagation, and MESSAGE_NOT_MODIFIED handling; change is backward-compatible with no API surface changes, touches only the Telegram adapter file, and carries a small risk if Telegram's error wording changes (regex scoped tightly); not manually end-to-end tested with a live bot.", + "dedupe_summary": "Telegram media messages fail to edit because editMessageText is used on captionable messages; fix is to auto-detect the \"no text in message\" 400 and retry with editMessageCaption or use editMessageReplyMarkup for buttons-only edits." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific description of the Telegram editMessageText->400 failure on captionable media and the retry-to-editMessageCaption / use editMessageReplyMarkup fix, which will match similar API-edit bugs." + }, + "usage": { + "input_tokens": 1466, + "output_tokens": 547 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "A prior change hardcoded disableBlockStreaming: true for WhatsApp, which unintentionally turned off block streaming so messages were batched and sent only when the agent finished, ignoring per-account or global blockStreaming settings.", + "solution_summary": "Restore config-driven behavior: the deliver callback now only suppresses tool payloads (not all payloads) and disableBlockStreaming is derived from channels.whatsapp.blockStreaming or agents.defaults.blockStreamingDefault, while upstream filtering continues to suppress reasoning payloads.", + "maintainer_signal_summary": "Fix verified with unit/integration tests and live WhatsApp checks; 13 inbound contract tests pass; backward compatible and no security/network changes; risk noted if upstream shouldSuppressReasoningPayload is removed.", + "dedupe_summary": "WhatsApp block streaming was disabled by a hardcoded flag; this PR makes block streaming respect per-account and global config and only suppresses tool/reasoning payloads appropriately." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change—removing a hardcoded disable for WhatsApp block streaming and making suppression limited to tool/reasoning payloads while honoring per-account/global config—without extraneous boilerplate." + }, + "usage": { + "input_tokens": 1458, + "output_tokens": 364 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Sending video media via the Telegram channel could make the gateway hang and become unresponsive because a content-length check failed before the response body was consumed, causing undici Agent.close() (closeDispatcher) to deadlock; the old default mediaMaxMb (5MB) also caused otherwise-valid files to be rejected.", + "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher to timeout and fall back to destroy to avoid Agent.close deadlocks, made media download errors non-fatal so captions still forward, and added timeouts and retry logic to getFile and file downloads; also extended inbound metadata stripping for UI.", + "maintainer_signal_summary": "Changes are backward compatible, no new dependencies, and configurable via channels.telegram.mediaMaxMb; tests updated (strip-inbound-meta.test.ts passing), verified end-to-end with a 7.5MB video, and key files touched include src/infra/net/ssrf.ts and src/telegram/bot.ts; risk noted for the 3s dispatcher timeout but mitigations are documented.", + "dedupe_summary": "Telegram gateway hangs on video uploads due to unconsumed response body causing undici Agent.close deadlock and strict 5MB media limit; fix increases media limit and adds dispatcher timeout/retries to prevent hangs." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary capturing the root cause (undici Agent.close deadlock from unconsumed response body), symptom (gateway hangs on video uploads), and the key fixes (increase media limit, add timeouts/retries), with no boilerplate noise." + }, + "usage": { + "input_tokens": 1554, + "output_tokens": 762 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "Telegram inbound body fallback always used the hardcoded \"\" when a message had media but no caption, so document, video, and other non-photo attachments were misidentified to the agent (e.g., PDFs appeared as images).", + "solution_summary": "Change resolveTelegramInboundBody to use the already-computed type-aware placeholder variable (producing , , etc.) for the fallback body text and add unit tests to cover media placeholder behavior and document message handling; media download and extraction logic remain unchanged.", + "maintainer_signal_summary": "This is a small, scoped bugfix in the Telegram integration that updates only the agent-facing body placeholder; new unit tests were added in extensions/telegram/src/bot/helpers.test.ts and extensions/telegram/src/bot.create-telegram-bot.test.ts to prevent regressions; closes #7116.", + "dedupe_summary": "Telegram integration mislabels non-photo media as in empty-body fallback instead of using a type-aware / placeholder." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific description of the Telegram integration bug (wrong fallback vs type-aware /), with no boilerplate—suitable for deduplication." + }, + "usage": { + "input_tokens": 1381, + "output_tokens": 450 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "MEDIA_MAX_BYTES was hardcoded to 5 MB in src/media/store.ts so files larger than 5 MB silently failed to stage into sandboxed agents, preventing common file types (PDFs, presentations, high-res images) from being processed.", + "solution_summary": "Introduced a new media.maxBytes configuration field, replaced the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes() accessors, and wired gateway boot to read the config (defaulting to 5 MB). Schema caps values at 500 MB and error messages now show the actual configured limit.", + "maintainer_signal_summary": "All 56 unit tests pass (store, server, staging, schema tests); bot review comments were addressed (reset behavior, MB formatting); change is backward compatible (default preserved, deprecated constant exported) and reversible by removing the config key; risks mitigated by schema max and TTL cleanup.", + "dedupe_summary": "Make media staging size limit configurable via media.maxBytes (override default 5 MB) to allow staging of larger files into sandboxed agents." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and focused: states the core change (make media.maxBytes configurable replacing hardcoded 5 MB limit) using specific technical terms and no template noise." + }, + "usage": { + "input_tokens": 1502, + "output_tokens": 477 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "When Telegram media download fails (due to exceeding configured mediaMaxMb or Telegram API errors), the handler returned early and silently dropped the entire message so the agent never saw it, received no ack reaction, and the user got no indication. This affects the Telegram integration message pipeline in src/telegram/bot-handlers.ts and processMessage dispatching.", + "solution_summary": "On download failure, extract file metadata (name, size, type) from the Telegram message object, build a metadata stub and inject it into msg.text, and let the message continue through the normal pipeline instead of returning early; previous automatic bot replies for these errors are removed so the agent can handle responses.", + "maintainer_signal_summary": "Bug fix targeting the Telegram integration (src/telegram/bot-handlers.ts). Backward compatible, low risk, reversible by reverting the single commit; note the change mutates msg.text only in the error path where messages previously were dropped.", + "dedupe_summary": "Telegram media download failures cause silent message drops; inject file metadata into message text and continue processing to avoid loss." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies the root cause (silent drops when Telegram media download fails) and the implemented fix (inject file metadata into message text and let the message continue), which is ideal for deduplication." + }, + "usage": { + "input_tokens": 1344, + "output_tokens": 479 + } + }, + { + "thread_id": 5796, + "number": 24117, + "kind": "pull_request", + "title": "feat(cli): add --session-key targeting for openclaw agent", + "summary": { + "problem_summary": "The openclaw agent CLI lacked a --session-key option, so CLI-triggered deliveries in multi-channel/multi-agent setups could resolve to the agent main session instead of an intended stable channel session because sessionId is UUID-based and external callers often can only supply a stable session key or lack a sender-derived --to.", + "solution_summary": "Adds a first-class --session-key flag to the openclaw agent command, surfaces sessionKey in AgentCliOpts and forwards it to resolveSessionKeyForRequest/gateway, updates help/validation text, keeps resolution precedence (sessionKey > sessionId > to > agent), and adds tests verifying CLI parsing and gateway forwarding.", + "maintainer_signal_summary": "Tests added for CLI parsing and gateway forwarding and are passing; change is additive and backward-compatible, small risk, follows existing resolver semantics and ready to merge.", + "dedupe_summary": "Add explicit --session-key support to openclaw agent CLI to target stable session keys for message/callback routing." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: states the CLI change (--session-key), target command (openclaw agent), and intent (use stable session keys for message/callback routing), with no boilerplate or noisy details." + }, + "usage": { + "input_tokens": 1316, + "output_tokens": 571 + } + }, + { + "thread_id": 766, + "number": 11611, + "kind": "pull_request", + "title": "feat: separate group-level allowlist from sender-level command authorization", + "summary": { + "problem_summary": "With groupPolicy: \"allowlist\" the current logic gates all group interactions (chat and slash commands) by sender phone number via groupAllowFrom, making it impossible to allowlist groups by JID, keep chat open to all participants in approved groups, or restrict slash commands to owners while preventing spam groups.", + "solution_summary": "When groupPolicy: \"allowlist\" and a channels.whatsapp.groups mapping exists, the access control now uses the groups mapping (group JIDs) as the group-level gate so approved groups accept messages from any participant while groupAllowFrom still controls slash command authorization; when no groups mapping is present, the legacy sender-based behavior is preserved.", + "maintainer_signal_summary": "Code changes update src/web/inbound/access-control.ts to call resolveChannelGroupPolicy, update JSDoc in src/config/types.whatsapp.ts, and add a Vitest suite (4 tests) covering allowlist behavior; this PR depends on #3326 and preserves backward compatibility, with one minor stale-comment doc fix noted.", + "dedupe_summary": "Separate group-level allowlist by group JID from sender-level command authorization: when a groups mapping exists, gate group membership by JID and keep slash commands restricted by groupAllowFrom." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core change—use groups JID mapping for group-level allowlist while preserving sender-based command authorization via groupAllowFrom—without noisy boilerplate." + }, + "usage": { + "input_tokens": 1366, + "output_tokens": 582 + } + }, + { + "thread_id": 17924, + "number": 46425, + "kind": "pull_request", + "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", + "summary": { + "problem_summary": "Configured mediaMaxMb for Telegram is ignored when delivering final reply messages containing media because loadWebMedia in the reply delivery path receives no maxBytes and falls back to the 100 MB default, while the direct-send path already honored the setting.", + "solution_summary": "Thread mediaMaxBytes through the Telegram outbound reply delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts and bot-native-commands.ts) so loadWebMedia receives and enforces the configured maxBytes; added unit tests verifying behavior and preserved default when unset.", + "maintainer_signal_summary": "Change is a small, backward-compatible bug fix limited to the Telegram integration, includes two unit tests and all related suites passing (108 tests), no security or API changes, and closes the linked issue.", + "dedupe_summary": "Telegram outbound reply media ignores mediaMaxMb (loadWebMedia lacked maxBytes); fix by passing mediaMaxBytes through outbound delivery chain to enforce limit." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that states the bug (outbound replies ignore mediaMaxMb), root cause (loadWebMedia lacked maxBytes), and the fix (thread mediaMaxBytes through delivery chain), making it ideal for deduplication." + }, + "usage": { + "input_tokens": 1367, + "output_tokens": 443 + } + }, + { + "thread_id": 5712, + "number": 37050, + "kind": "pull_request", + "title": "fix(telegram): add HTML parse fallback for media captions", + "summary": { + "problem_summary": "Telegram media sends (photo, document, video, animation, audio, voice, video note) could fail with a 400 \"can't parse entities\" error when captions contained invalid HTML entities because the media send path did not use the HTML-parse fallback used by text messages.", + "solution_summary": "Wrap the media send callback in withTelegramHtmlParseFallback so that on an HTML parse error the send is retried without parse_mode and with the raw caption (plain text); this change applies to all media types and leaves caption-splitting and other logic unchanged.", + "maintainer_signal_summary": "Small, backward-compatible bug fix in src/telegram/send.ts with a new unit test verifying the retry behavior; no config changes, text/edit paths unaffected, and the change can be reverted by restoring the send.ts modification.", + "dedupe_summary": "Telegram media caption HTML-parse errors cause 400 failures; add fallback to retry media sends as plain text without parse_mode." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies HTML parse errors in Telegram media captions causing 400 failures and the added fallback retrying sends as plain-text without parse_mode, with no irrelevant boilerplate." + }, + "usage": { + "input_tokens": 1241, + "output_tokens": 706 + } + }, + { + "thread_id": 21769, + "number": 55280, + "kind": "pull_request", + "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", + "summary": { + "problem_summary": "WhatsApp had block streaming effectively disabled and a delivery race: block replies were enqueued optimistically and the pipeline marked content as sent before actual delivery, which could cause neither intermediate blocks nor the final reply to be delivered. A ternary bug also caused configured blockStreaming to be ignored (undefined vs false).", + "solution_summary": "Expose block streaming as channels.whatsapp.blockStreaming, fix process-message to correctly resolve the config, add sensible coalescing defaults, implement sendBlockReplyAsync to await confirmed delivery, and update onBlockReply to await that promise so sentContentKeys are only marked after success; tests updated accordingly.", + "maintainer_signal_summary": "Changes touch process-message.ts, reply-dispatcher.ts, dispatch-from-config.ts, shared.ts and tests; includes unit and manual verification showing both streaming-on and off behavior, and explicitly fixes the root cause rather than the prior workaround.", + "dedupe_summary": "WhatsApp block-streaming disabled/hardcoded and delivery race where optimistic enqueue suppressed final replies; needs configurable streaming and confirm-on-delivery for block replies." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific summary that captures the core change (make WhatsApp block-streaming configurable) and the root cause (delivery race/optimistic enqueue suppressing final replies) without boilerplate, so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 1154, + "output_tokens": 742 + } + }, + { + "thread_id": 8098, + "number": 25543, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked a group-level allowlist option, so administrators could not whitelist specific Signal groups by ID to bypass sender-level groupAllowFrom checks or allow groups to run control commands, unlike other channels (Telegram, WhatsApp, etc.).", + "solution_summary": "Add channels.signal.groups. config with a SignalGroupConfig type and Zod schema, wire resolveChannelGroupPolicy into the Signal event handler to grant group-level bypass of sender-level checks, and incorporate groupExplicitlyAllowed into commandGate authorizers; update docs and types accordingly.", + "maintainer_signal_summary": "PR is safe to merge per review: TypeScript compilation passes, manually tested on a live Signal instance, addresses Greptile feedback by including group-level authorization in command gating, and preserves backward compatibility via hasGroupAllowFrom handling (confidence reported 5/5).", + "dedupe_summary": "Add group-level allowlist support for Signal (channels.signal.groups.) to bypass sender-level groupAllowFrom checks and permit control commands for explicitly allowed groups." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states the core change (new channels.signal.groups. allowlist), its effect (bypass sender-level groupAllowFrom) and purpose (permit control commands for allowed groups), with no boilerplate or noise." + }, + "usage": { + "input_tokens": 1096, + "output_tokens": 530 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" and groupAllowFrom listed group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter and never matches group IDs.", + "solution_summary": "Add a groups config to SignalAccountSchemaBase (same shape as other channels) and call resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom; add four tests to cover group policy resolution.", + "maintainer_signal_summary": "Small, targeted fix (label: channel: signal, size: S) that adds schema and event-handler logic; all relevant tests (config, Signal monitor, group-policy) pass and four new tests added; fixes #25540.", + "dedupe_summary": "Signal missing groups config / resolveChannelGroupPolicy caused allowlist group messages to be dropped; add group-level allowlist support." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise technical summary that identifies Signal lacked a groups config causing group-level allowlist checks to fail and states the fix to add group-level allowlist support." + }, + "usage": { + "input_tokens": 896, + "output_tokens": 539 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "WhatsApp channel hardcodes disableBlockStreaming:true in process-message.ts, so when agents send multiple messages with tool calls/sleeps in between all text is coalesced into one final WhatsApp message; delivery logic also incorrectly treated only queuedFinal as marking a reply delivered, ignoring intermediate blocks.", + "solution_summary": "Make WhatsApp honor channels.whatsapp.blockStreaming by adding resolveWhatsAppBlockStreamingEnabled and replacing the hardcoded flag with the config value, fix delivery counting so intermediate blocks can count as delivered, register blockStreaming capability and default in dock, and update tests accordingly.", + "maintainer_signal_summary": "Small, backward-compatible change with updated tests that aligns WhatsApp behavior with Telegram/Line; labeled for whatsapp-web, web-ui and agents and ready for review.", + "dedupe_summary": "WhatsApp always coalesces message blocks due to hardcoded disableBlockStreaming; change to config-driven blockStreaming so intermediate blocks are delivered separately." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it identifies the hardcoded disableBlockStreaming causing WhatsApp to coalesce message blocks and states the change to make block streaming config-driven so intermediate blocks are delivered separately." + }, + "usage": { + "input_tokens": 646, + "output_tokens": 607 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v10-minimal-rules.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v10-minimal-rules.json new file mode 100644 index 0000000..09f15fa --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v10-minimal-rules.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v10-minimal-rules", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "Summarize this GitHub issue/PR for duplicate detection. Strip template boilerplate and testing notes. Focus on the specific technical problem or change.\n\nReturn JSON: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Plain text, 1-3 sentences each.", + "timestamp": "2026-03-30T16:14:26.454Z", + "aggregate": { + "avg_score": 4.1, + "avg_signal_density": 4.03, + "boilerplate_count": 10, + "boilerplate_pct": 25, + "would_cluster_correctly_pct": 95, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 40703, + "total_output_tokens": 25497 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "channels.discord.proxy only proxies the Discord WebSocket Gateway connection; startup REST API requests (command deployment, fetch-bot-identity) and guild resolution do not use the configured HTTP proxy, causing REST calls to time out with \"fetch failed\" and guild channels to be unresolved and non-functional behind an HTTP proxy (DMs via WebSocket still work).", + "solution_summary": "Ensure the configured channels.discord.proxy (or a new global proxy setting) is applied to the undici dispatcher used for all REST API calls and guild-resolve code paths (not just the WebSocket), e.g., install a ProxyAgent globally or pass a proxied dispatcher during Discord provider startup; add retry handling for guild resolve. ", + "maintainer_signal_summary": "Regression report with OpenClaw 2026.3.13, Node 25.8.0 (undici fetch), reproducible behind corporate proxy; user provides logs, repro steps, and a partial workaround (NODE_OPTIONS + ProxyAgent fixes REST but not guild resolve). Labels: bug, regression — needs fix to make proxy support complete. ", + "dedupe_summary": "Fingerprint: Discord channel proxy regression — websocket proxied but undici/REST and guild-resolve not proxied, causing \"fetch failed\" and \"channels unresolved: guild:\"; OpenClaw 2026.3.13, Node 25/undici; workaround using global ProxyAgent partially succeeds (REST) but guild resolve still fails." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Clear, specific description of the core regression (WebSocket proxied but undici/REST and guild-resolve not), but it includes extraneous version and workaround details that aren't needed for deduplication." + }, + "usage": { + "input_tokens": 2153, + "output_tokens": 618 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "Outbound Discord REST requests (message sends and attachment transfers) do not honor channels.discord.proxy; gateway/WebSocket connections work via the proxy but RequestClient.fetch has no proxy support, producing “fetch failed” errors in restricted-network environments.", + "solution_summary": "Implement proxy support in the RequestClient.fetch path and ensure all REST/media requests use channels.discord.proxy (support HTTP/SOCKS/TUN). Concretely: inject an appropriate HTTP/SOCKS agent or use a proxy-aware fetch wrapper that reads the configured proxy URL and is applied to message send, attachment upload/download, and other REST calls.", + "maintainer_signal_summary": "Reporter filed a bug on v2026.2.26 for users in China and labeled it bug; there is no maintainer response in the thread and the report requests prioritization to restore functionality in restricted regions.", + "dedupe_summary": "Check for existing issues named or tagged: 'discord proxy', 'channels.discord.proxy not applied', 'RequestClient fetch proxy', 'Discord REST proxy', or 'fetch failed proxy'. If any match, mark duplicates of the RequestClient/REST proxy gap." + }, + "judge": { + "score": 3, + "has_boilerplate": true, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Includes the right technical keywords (Discord REST proxy, channels.discord.proxy, RequestClient/fetch) that will match duplicates but is phrased as triage instructions rather than a concise problem statement, so it's useful but noisy." + }, + "usage": { + "input_tokens": 835, + "output_tokens": 804 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "When OpenClaw’s Discord provider runs behind an HTTP proxy, the gateway/login path succeeds but some REST startup calls consistently fail with “fetch failed” (notably native command deployment and bot identity fetch). The symptoms show proxy is enabled and applicationId fetch works, but deploy-rest and fetch-bot-identity requests fail while the WebSocket/gateway remains connected.", + "solution_summary": "Ensure all Discord REST requests use the same proxy-aware HTTP client/dispatcher as the gateway/login path—propagate channels.discord.proxy into every REST call path or unify to a single HTTP client implementation; add targeted logging and retries for startup REST calls to surface proxy misconfiguration and transient errors.", + "maintainer_signal_summary": "Reproducible on OpenClaw 2026.3.12 (service touched by 2026.3.13) with channels.discord.proxy set; likely a bug where some REST request paths don’t pick up proxy settings. This should be investigated by checking where the proxy setting is applied and harmonizing the fetch/dispatcher usage across startup REST flows.", + "dedupe_summary": "Search existing issues for keywords: \"fetch failed\", \"discord rest proxy\", \"deploy-rest:put:error\", \"fetch-bot-identity\", \"channels.discord.proxy\", and \"stale-socket\"; include OpenClaw versions 2026.3.12/2026.3.13 and sample log lines showing deploy-rest:put:error and logged in to discord. " + }, + "judge": { + "score": 2, + "has_boilerplate": true, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "The summary is framed as search/instructional text with versions and sample logs instead of a concise statement of the core problem (Discord REST calls like deploy/identity failing behind an HTTP proxy), so it contains noise and won't reliably cluster duplicates." + }, + "usage": { + "input_tokens": 981, + "output_tokens": 670 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "OpenClaw recognizes the Discord channel proxy in its logs but Discord REST calls and gateway connection fail with \"fetch failed\" under WSL2 when an HTTP proxy is required, despite curl succeeding through the same proxy. The issue is reproducible on OpenClaw 2026.3.13 with proxy set at ~/.openclaw/openclaw.json → channels.discord.proxy and results in gatewayConnected=false and no WebSocket establishment.", + "solution_summary": "Ensure the configured proxy is actually applied to Discord's underlying HTTP client (fetch/undici used by discord.js) and to the gateway connection, e.g., pass a proxy agent or appropriate undici/fetch proxy settings (or honor HTTP_PROXY/HTTPS_PROXY) so REST and WebSocket requests route through the proxy.", + "maintainer_signal_summary": "Reporter provided reproduction steps, config snippet, environment (WSL2), logs showing proxy-enabled messages but fetch failures, and notes high severity and 100% reproducibility; no maintainer response in the thread.", + "dedupe_summary": "Distinctive for OpenClaw 2026.3.13 on WSL2 where channels.discord.proxy is logged as enabled but REST API errors show \"fetch failed\"; likely duplicates are other reports about discord.js/undici proxy not being applied or gateway REST failing when using an HTTP proxy." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Summary captures the core issue (configured HTTP proxy is logged as enabled but Discord REST calls fail and points to discord.js/undici proxy not being applied) but includes minor noise (specific app version and environment) and extra phrasing that aren't needed for deduping." + }, + "usage": { + "input_tokens": 818, + "output_tokens": 612 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "OpenClaw's Discord REST API calls ignore configured proxy settings (channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY) and fail with \"TypeError: fetch failed\", while the Discord WebSocket and other channels (e.g., Telegram) work and curl through the proxy succeeds.", + "solution_summary": "Route Discord REST API HTTP requests through the same proxy-aware SSRF guard / transport fetch path used for web_search/web_fetch (or otherwise ensure the fetch uses the configured proxy agent) so Discord REST calls honor proxy configuration.", + "maintainer_signal_summary": "Reporter on v2026.2.26 provides config, env vars, logs, and repro steps showing WebSocket works but REST fails; changelog notes a proxy fix for web tools that explicitly did not include Discord REST, indicating an omission; curl through proxy succeeds so proxy is functional.", + "dedupe_summary": "Duplicate if issue reports 'fetch failed' or Discord REST failing to use channels.discord.proxy or HTTP_PROXY/HTTPS_PROXY, mentions gateway v2026.2.26 (or similar) and that web_search/web_fetch proxy fix excluded Discord REST." + }, + "judge": { + "score": 4, + "has_boilerplate": true, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Captures the core problem (Discord REST/fetch failing to honor channels.discord.proxy or HTTP(S)_PROXY and exclusion from a previous web_search/web_fetch proxy fix) but includes meta 'Duplicate if...' phrasing and a version number that are unnecessary noise." + }, + "usage": { + "input_tokens": 841, + "output_tokens": 710 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "The CLI command openclaw agent --channel discord --to resolves every delivery to agent:*:main instead of the live Discord channel/thread session because the session resolver does not search by real delivery context (channel + to + accountId), the command path did not propagate channel/accountId, Discord targets are not normalized, and compact resolver lookup logic is incorrect.", + "solution_summary": "Manual patches to compiled dist files: gateway-cli now resolves sessions by delivery target and normalizes Discord targets; compact uses lookup by delivery context and buildAgentMainSessionKey; register.agent passes channel and replyAccount/accountId to the resolver. Post-patch validation shows CLI routes to the correct Discord session.", + "maintainer_signal_summary": "High regression risk: fixes were applied directly to compiled files in dist/ (dist/gateway-cli-BjsM6fWb.js, dist/compact-1mmJ_KWL.js, dist/register.agent-DHoJWl4M.js) and will be lost on updates. Recommendation: implement the fix in source, add tests, and include a post-update revalidation step.", + "dedupe_summary": "Duplicate signature: CLI agent Discord delivery context resolver bug causing messages to route to agent:*:main; key artifacts changed are dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js; related issues #36401, #41562, #16149." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summarizes the core bug (CLI Discord delivery-context resolver routing to agent:*:main) and relevant compiled artifacts, but includes extraneous related-issue references that aren't needed for deduplication." + }, + "usage": { + "input_tokens": 768, + "output_tokens": 573 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions: Discord threads had volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) re-injected every turn causing context bloat and degradation; CLI agent delivery resolved to the global main session instead of the thread-bound live session because the session resolver didn't use the actual delivery context (channel+to+accountId).", + "solution_summary": "Patched compiled runtime bundles to stop reinjecting the volatile Discord metadata and to resolve sessions by actual delivery target. Changes: pi-embedded stopped injecting the listed metadata fields; gateway-cli normalizes Discord targets and resolves by delivery target; compact added delivery-context lookup for agent CLI; register.agent now passes channel and replyAccount/accountId and fixes incorrect session-key usage. Verified by restarting the gateway service and observing correct sessions and no repeated context.", + "maintainer_signal_summary": "Fixes were applied directly to compiled files in openclaw/dist (dist/pi-embedded-..., dist/gateway-cli-..., dist/compact-..., dist/register.agent-...), so the patches are manual and will be lost on update—maintainers should add these fixes to source, or maintain a re-application script post-update. Target environment: OpenClaw 2026.3.11, Discord channel, systemd deployment.", + "dedupe_summary": "Unique issue: 'discord-thread volatile metadata reinjection + CLI session resolver using main instead of thread-bound session'. Matches reports about repeated Discord-thread context bloat or CLI commands landing in agent:*:main; resolved by stopping metadata reinjection and adding delivery-context session resolution in the listed bundles." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures both core regressions (Discord-thread metadata reinjection and CLI session resolver routing to agent:*:main) and the fix approach, with no boilerplate or unrelated detail." + }, + "usage": { + "input_tokens": 687, + "output_tokens": 666 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "Sandbox media staging cap is hardcoded to 5 MB in dist/store-*.js (const MEDIA_MAX_BYTES = 5 * 1024 * 1024), so files larger than 5 MB (PDFs, PPTX, images) cannot be copied into sandboxed agents. Per-type config keys (tools.media.image.maxBytes, tools.media.audio.maxBytes) exist but there is no way to override this global staging cap. Regression observed since v2026.3.7 and present in v2026.3.8.", + "solution_summary": "Expose the staging cap as a configurable setting (e.g. tools.media.maxBytes in openclaw.json) with a sensible default (5 MB) and update the build/runtime to read and apply that value before per-type checks; add tests and documentation for the new config. Ensure the change is applied to dist files so npm installs don't require manual patching.", + "maintainer_signal_summary": "This is a user-visible regression and should be fixed in a patch release; a manual sed-based workaround exists but is not acceptable long-term. Prioritize a small change to load the config value and release an update.", + "dedupe_summary": "Likely duplicates: other issues about media size limits, sandbox staging limits, or missing tools.media.* configuration. Search for 'MEDIA_MAX_BYTES', 'sandbox media limit', or 'tools.media.maxBytes' before adding a duplicate issue." + }, + "judge": { + "score": 3, + "has_boilerplate": true, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Includes the important technical terms (MEDIA_MAX_BYTES, sandbox media limit, tools.media.maxBytes) but is phrased as triage/instruction rather than a concise 1-2 sentence summary, so it's useful but noisy." + }, + "usage": { + "input_tokens": 734, + "output_tokens": 835 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "Discord bot receives messages (Gateway/WS) but all REST API calls fail with TypeError: \"fetch failed\" despite openclaw.json showing proxy configured and logs saying \"rest proxy enabled\". Direct curl calls using the same proxy succeed, so the REST proxy path in OpenClaw appears to be broken or not used for HTTP requests.", + "solution_summary": "No fix proposed in the report. User demonstrates the proxy works externally (curl) and only OpenClaw's REST requests fail, implying a bug in OpenClaw's REST proxy handling; no workaround or code change was supplied.", + "maintainer_signal_summary": "Issue is labeled as a bug and includes logs showing \"rest proxy enabled\" and \"gateway proxy enabled\", but there are no maintainer comments or PRs referenced. The report includes environment details (OpenClaw 2026.2.25, macOS, Node 25.6.1).", + "dedupe_summary": "Unique identifiers: REST \"fetch failed\" while Gateway works; openclaw.json proxy field set and logged as enabled; curl with same proxy succeeds; OpenClaw v2026.2.25 on Node 25.6.1 — likely duplicates are issues about REST proxy not being applied or node-fetch/undici proxy integration failures." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise summary that captures the core problem (REST API requests failing with \"fetch failed\" despite proxy configured and gateway working) but includes minor noise like explicit version numbers and a speculative duplicate note that should be removed for perfect deduping." + }, + "usage": { + "input_tokens": 679, + "output_tokens": 750 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "Discord REST API calls (via @buape/carbon's RequestClient) ignore channels.discord.proxy from openclaw.json because RequestClient calls globalThis.fetch without a proxy-aware dispatcher; Node.js 22's native fetch doesn’t use HTTP_PROXY/HTTPS_PROXY unless --use-env-proxy is set, which breaks WebSocket connections.", + "solution_summary": "Make @buape/carbon's REST client accept and use an optional undici dispatcher or explicit proxy option, and have OpenClaw pass channels.discord.proxy into it; as a temporary workaround, preload a module to call setGlobalDispatcher(new EnvHttpProxyAgent()) instead of using --use-env-proxy.", + "maintainer_signal_summary": "This is a transport/config bug affecting REST calls, memorySearch embeddings, and attachment downloads in proxied environments and should be prioritized; the correct fix is wiring a proxy-aware dispatcher into the RequestClient (and related fetch paths) rather than using --use-env-proxy which breaks gateway websockets.", + "dedupe_summary": "Related to #25232 (attachments bypass proxy) — same root cause (fetch/dispatcher not proxy-aware) but different code paths; link issues and fix dispatcher usage across components rather than treating them as exact duplicates." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise identification of the root cause ('fetch/dispatcher not proxy-aware') that links related reports, but includes minor meta/instructional wording and omits the specific component name which slightly reduces precision." + }, + "usage": { + "input_tokens": 688, + "output_tokens": 876 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "Under sustained load with long-running conversations OpenClaw shows severe performance degradation: unbounded context token growth (sessions up to ~42k/90k tokens) that increases per-turn latency, Discord lane blocking causing serialization, and typing indicators timing out or remaining until TTL.", + "solution_summary": "Applied emergency config changes to limit concurrency and context size (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents.maxConcurrent 8→4, maxChildrenPerAgent 8→4, agents.defaults.contextTokens 80000) and increased Discord eventQueue.listenerTimeout 120000→300000; also improved patching script to cover more hash variants and restarted the gateway, with no new lane/typing errors observed post-restart.", + "maintainer_signal_summary": "Maintainers implemented immediate mitigations, updated the patching script, and verified the gateway is running without new errors; they note restart helps but recommend making limits adaptive or configurable for a long-term fix.", + "dedupe_summary": "Related/overlapping with #27690 and #44026 (lane blocking/serialization) and typing TTL issues (#26733/#27172); this report is distinct in providing concrete config changes and verification steps but duplicates the underlying causes (unbounded context growth and excessive concurrency)." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Captures the core causes (unbounded context growth and excessive concurrency) and links related issues, but includes minor noise about config/verification steps and explicit issue references that aren't needed for deduping." + }, + "usage": { + "input_tokens": 622, + "output_tokens": 663 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord thread handling re-injects volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, causing context contamination, token growth, prompt cache misses and performance degradation in long threads. The compiled runtime lacks a guard to limit this injection to the first turn.", + "solution_summary": "Manual edits to compiled files in dist/ prevented repeated injection and added a first-turn guard: modified dist/pi-embedded-D6PpOsxP.js to stop injecting the listed fields and updated dist/compact-1mmJ_KWL.js to add an isNewSession guard and replace resolveAgentMainSessionKey with buildAgentMainSessionKey(...). Post-patch validation shows threads no longer echo repeated context and prompt caching is restored.", + "maintainer_signal_summary": "High regression risk because fixes were applied directly to compiled dist/ files and will be lost on upgrade; maintainers should implement the fix in source, replace the patched logic in the build, and add a post-update revalidation script. Related issues: #41355, #34228, #37047.", + "dedupe_summary": "Keywords for dedupe: discord thread, thread starter re-injected every turn, metadata injection (message_id reply_to_id timestamp Chat history), prompt cache bust, dist/* compiled patch, isNewSession guard, buildAgentMainSessionKey replacement." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "The summary captures the core bug—Discord thread metadata (starter/history/message ids/timestamps) being re-injected every turn causing prompt cache busts—and includes useful technical anchors (dist files, isNewSession guard), though it's presented as keywords rather than a concise sentence and includes minor build/patch noise." + }, + "usage": { + "input_tokens": 634, + "output_tokens": 611 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "In v2026.3.11 Discord thread messages repeatedly inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, degraded performance, token bloat and prompt-cache busting.", + "solution_summary": "Applied a manual patch to dist/pi-embedded-D6PpOsxP.js to prevent unconditional metadata injection for Discord threads — metadata is now injected only on the first turn; validation shows repeated context removed and prompt caching restored.", + "maintainer_signal_summary": "HIGH regression risk: change was made to a compiled file in dist and will be lost on update/reinstall; maintainers should apply an official source-level fix and keep a post-update revalidation script.", + "dedupe_summary": "Related/duplicate reports: #41355, #34228, #37047 — all describe Discord thread starter/history re-injection or thread session resets and should be linked/considered duplicates." + }, + "judge": { + "score": 3, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "The summary contains the key technical phrase (Discord thread starter/history re-injection) useful for deduping but is too terse and does not succinctly state the core bug (metadata re-injection every turn) or root cause." + }, + "usage": { + "input_tokens": 642, + "output_tokens": 618 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "Two runtime regressions in Discord threads: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing unbounded context growth and a session resolver that falls back to a global agent (agent:*:main) instead of selecting live thread-bound sessions, leading to wrong CLI/session routing and increasing latency over long threads.", + "solution_summary": "Applied manual runtime patches to compiled bundles to stop repeated metadata injection and to make the resolver lookup by delivery context (channel + to + accountId); modified dist/reply-BCcP6j4h.js, dist/compact-1mmJ_KWL.js, dist/pi-embedded-Cz5VjpnY.js, dist/gateway-cli-B-E8XzUM.js, dist/register.agent-jS8B5meo.js, added an idempotent re-application script and backups, and restarted the gateway.", + "maintainer_signal_summary": "Hotpatches were applied to production bundles and the gateway was restarted and validated (no new errors, thread-bound sessions resolving, CLI routing correct); maintainers note these are manual edits that will be lost on updates and provided an idempotent reapply script plus backups for operational continuity.", + "dedupe_summary": "Keywords: discord threads, context growth, repeated metadata injection, session resolver fallback to agent:*:main, CLI routing wrong, patched dist/*.js, hotpatch with reapply script, OpenClaw 2026.3.11." + }, + "judge": { + "score": 4, + "has_boilerplate": true, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Captures the core bug (Discord thread context growth from repeated metadata and session resolver fallback causing CLI routing to wrong session) and modified artifacts, but includes an unnecessary version number and a minor operational detail (hotpatch/reapply) that aren't helpful for deduplication." + }, + "usage": { + "input_tokens": 618, + "output_tokens": 594 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "After updating, OpenClaw receives Discord messages and generates responses but fails to send replies with \"TypeError: fetch failed\" despite logs showing the Discord REST and gateway proxies enabled and curl succeeding through the same proxy. Environment: OpenClaw 2026.3.2 on Node.js 22.22.0 (Linux systemd); proxy is http://127.0.0.1:7890 (Clash).", + "solution_summary": "Ensure the HTTP(S) proxy is actually applied to the fetch calls used to post replies (including any multipart/attachment endpoints) — either by passing a proxy/agent to the fetch implementation used by the Discord client, setting HTTP_PROXY/HTTPS_PROXY for the systemd service, or switching to a fetch/undici configuration that supports proxying. Add logging/tests for the specific message-send code path to confirm the agent is used. ", + "maintainer_signal_summary": "Investigate the reply-send code path to find where fetch is invoked without a proxy agent or environment variables; add a fix to propagate the configured proxy to all REST requests (including file uploads), add unit/integration tests, and confirm behavior under systemd environment variables. ", + "dedupe_summary": "Likely duplicate of issues where native fetch/undici in Node ignores proxy settings or specific Discord message endpoints bypass the configured REST proxy; also related to problems where systemd service lacks HTTP_PROXY/HTTPS_PROXY env vars or where multipart uploads don't use the proxy agent." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summarizes specific likely root causes (native fetch/undici ignoring proxy, Discord REST bypass, missing HTTP_PROXY/HTTPS_PROXY in systemd, multipart uploads not using proxy) that are relevant for clustering, though it lists multiple hypotheses rather than a single concise cause." + }, + "usage": { + "input_tokens": 558, + "output_tokens": 818 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "In v2026.3.11 the CLI command `openclaw agent --channel discord --to ...` resolves to the fallback session `agent:*:main` instead of the live Discord channel/thread session because the compiled session resolver does not lookup sessions by delivery context and fails to normalize Discord targets.", + "solution_summary": "A manual patch was applied to dist/gateway-cli-BjsM6fWb.js to normalize Discord targets (e.g. `148...` → `channel:148...`) and add a session lookup by delivery context (channel+to+accountId) before falling back to main, restoring correct routing to the live session.", + "maintainer_signal_summary": "Patch was made directly to compiled dist files (high regression risk and will be lost on update); this needs a proper fix in source code and build artifacts plus tests to persist. Related issues: #36401 and #41562; user requests an official fix to avoid repeated hotfixes.", + "dedupe_summary": "Distinctive fingerprint: v2026.3.11 compiled CLI (dist/gateway-cli-*.js) lacking delivery-context session lookup and Discord target normalization, causing fallback to agent:*:main; may overlap with other session-routing/Discord-thread issues (e.g. #41562) but is specific to the compiled-dist hotfix scenario." + }, + "judge": { + "score": 4, + "has_boilerplate": true, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary clearly captures the core bug (compiled CLI dist/gateway-cli-*.js missing delivery-context lookup and Discord target normalization causing fallback to agent:*:main) but includes minor noisy elements like a version/issue reference and “Distinctive fingerprint” phrasing that aren’t needed for deduping." + }, + "usage": { + "input_tokens": 623, + "output_tokens": 848 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "In v2026.3.11 the compiled file dist/compact-1mmJ_KWL.js was patched to add delivery context lookup but incorrectly calls a non-existent function resolveAgentMainSessionKey, causing ReferenceError at runtime and breaking the agent command (possible gateway crashes).", + "solution_summary": "Locally patched dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with the correct buildAgentMainSessionKey(...); this removes the ReferenceError and restores agent/session behavior. The fix is applied to the compiled output and validated.", + "maintainer_signal_summary": "High priority: this is a regression introduced by the previous session-resolver fix (#3) and will recur if the original source/patch is reapplied; the correct change must be made in source and recompiled rather than only editing dist/ to avoid losing the fix on upgrades.", + "dedupe_summary": "Regression: ReferenceError: resolveAgentMainSessionKey is not defined in dist/compact-*.js due to incorrect function name used by the session-resolver patch; correct function is buildAgentMainSessionKey." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that names the regression, error, affected compiled file pattern, incorrect function used, and the correct function to match duplicate reports." + }, + "usage": { + "input_tokens": 594, + "output_tokens": 533 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "The discord channel's proxy setting (channels.discord.proxy) is only applied to WebSocket connections, not the REST HTTP client. As a result health checks and message sends behind a proxy time out with \"fetch failed\" while direct curl via the same proxy succeeds.", + "solution_summary": "Make the Discord plugin use the configured channels.discord.proxy for its REST HTTP client and health check (not just WebSocket), so all Discord HTTP requests route through the proxy; add/update tests and ensure the gateway/CLI uses the same proxied client.", + "maintainer_signal_summary": "Regression: previously worked, now fails in OpenClaw 2026.3.8 (commit 3caab92). Repro steps provided (set channels.discord.proxy, curl works, openclaw message send fails). Affects users behind proxies and blocks send/health functionality.", + "dedupe_summary": "Keywords: \"fetch failed\", channels.discord.proxy, Discord REST API proxy, WebSocket-only proxy, openclaw message send timeout, health check fetch failed, OpenClaw 2026.3.8, gateway → discord channel." + }, + "judge": { + "score": 3, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "The summary contains the core signals (channels.discord.proxy not applied to Discord REST API, health check 'fetch failed', WebSocket-only proxy) but is presented as a noisy keyword list, includes an unnecessary version number, and lacks a concise 1–3 sentence description." + }, + "usage": { + "input_tokens": 550, + "output_tokens": 689 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "En v2026.3.11, el session resolver dentro de dist/compact-1mmJ_KWL.js no realiza lookup por delivery context (channel+to+accountId), por lo que las sesiones se resuelven incorrectamente a agent:*:main en lugar de la sesión viva del canal objetivo.", + "solution_summary": "Parche manual en dist/compact-1mmJ_KWL.js: se añadió lookup por delivery context (channel+to+accountId) en el resolver del comando agent y el fallback a main solo se usa si no existe una sesión específica.", + "maintainer_signal_summary": "Alto riesgo de regresión: el cambio fue aplicado en archivos compilados en dist y se perderá con futuras actualizaciones; la corrección debe incorporarse al código fuente y recrearse en el build/release.", + "dedupe_summary": "Coincidir duplicados por versión (v2026.3.11), ruta de archivo (dist/compact-1mmJ_KWL.js) y síntoma (resolver no busca delivery-context y cae a agent:*:main) para distinguirlo de otros problemas de sesión." + }, + "judge": { + "score": 3, + "has_boilerplate": true, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Summary captures the key symptom and file path (session resolver in dist/compact-1mmJ_KWL.js lacking delivery-context lookup) but includes version metadata and meta-instruction phrasing instead of a concise problem statement, adding noise." + }, + "usage": { + "input_tokens": 564, + "output_tokens": 770 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "The Control UI has most interface text hardcoded in English (settings, buttons, agent/skill pages, channel configs, status messages), creating usability barriers for non-English users; compiled assets contain literal strings like \"Settings\", \"Add\", \"Delete\", etc.", + "solution_summary": "Extract UI strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, provide a language selector or auto-detect system language, and build infrastructure to add more languages later.", + "maintainer_signal_summary": "No maintainer comments in the report; the author requests roadmap status and contributors, noting docs already include zh-CN which signals project interest in Chinese support.", + "dedupe_summary": "Likely a standalone feature request for UI localization but may overlap with any existing i18n, localization, or UI internationalization/refactor issues—check for prior i18n/translation or UI refactor tickets before acceptance." + }, + "judge": { + "score": 2, + "has_boilerplate": true, + "signal_density": 2, + "would_cluster_correctly": false, + "reasoning": "The summary is vague and meta—it notes a UI localization feature but omits key specifics (OpenClaw Control UI, zh‑CN, extracting strings) and includes procedural/acceptance instructions, so it contains boilerplate and would likely cause noisy or incorrect clustering." + }, + "usage": { + "input_tokens": 485, + "output_tokens": 493 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "WhatsApp group chats could not use the channel-level systemPrompt at account or group granularity, preventing per-account or per-group prompt customization and wildcard defaults for groups without their own prompt.", + "solution_summary": "Added systemPrompt support to WhatsApp config/schema at channel, account, and group levels; implemented resolution hierarchy where account-level prompt overrides root, group prompt resolves to specific group then wildcard, account.groups fully replace root.groups, and the final injected prompt is account prompt + resolved group prompt. Included schema/types changes, account resolution merge, group prompt composition, tests and docs.", + "maintainer_signal_summary": "PR includes tests and docs, closes the linked issue, is backward-compatible with no security/network changes, the author addressed bot review conversations, and this is a focused replacement for a previous larger PR; no changes to non-WhatsApp channels or hot-reload behavior.", + "dedupe_summary": "WhatsApp: add account/group systemPrompt hierarchy (account overrides root, group specific→wildcard fallback, account.groups replace root.groups) and compose final prompt as account + group." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused on the WhatsApp systemPrompt hierarchy and final composition, with specific behaviors (account overrides, wildcard fallback, account.groups replacing root.groups) that will help cluster similar changes." + }, + "usage": { + "input_tokens": 2261, + "output_tokens": 549 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages received via messages.upsert(type=\"append\") were being dropped before agent processing, and WhatsApp group policy helpers resolved without accountId so account-scoped settings (channels.whatsapp.accounts. / groupAllowFrom / allowFrom) could be ignored, causing allowlist fallback to fail closed.", + "solution_summary": "Enable append-path processing only for WhatsApp group messages (leave DM append skipped) and propagate accountId / resolve the WhatsApp account when evaluating group gating so account-scoped groupAllowFrom/allowFrom are considered for allowlist fallback.", + "maintainer_signal_summary": "Bug fix touching WhatsApp inbound and group activation code (e.g. src/web/inbound/monitor.ts, src/web/auto-reply/monitor/group-activation.ts). No config schema or dist changes; tests added and local traces validated that groups now enqueue to the agent and account-scoped gating behaves correctly. Risk: historical group backlog may be processed; rollback by reverting the PR or restoring the two touched files.", + "dedupe_summary": "Two distinct issues fixed: (1) group append events were skipped and never enqueued to the agent; (2) group policy resolution omitted accountId so account-scoped allowlist/require-mention settings could be ignored or fail closed." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the two technical bugs (WhatsApp group append events not enqueued; policy resolution missing accountId affecting account-scoped allow/require-mention) with no template noise, suitable for deduplication." + }, + "usage": { + "input_tokens": 1813, + "output_tokens": 558 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Canvas (A2UI) returned 401 when accessed via Tailscale Serve because resolveClientIp() returned undefined for requests from a trusted proxy that omitted X-Forwarded-* headers, and isLocalDirectRequest() did not fall back to the socket remoteAddress when allowRealIpFallback was enabled and XFF resolved to a non-loopback Tailscale IP.", + "solution_summary": "Fixes: resolveClientIp() now returns the socket address when no forwarding headers are present; isLocalDirectRequest() checks req.socket.remoteAddress when allowRealIpFallback is enabled; isLocalishHost() now requires an allowTailscale flag to accept .ts.net hosts; auth.allowTailscale is threaded through callers and docs updated with required gateway.trustedProxies and allowRealIpFallback guidance. No new config keys introduced.", + "maintainer_signal_summary": "Depends on PR #50636 and closes #28472; tests updated and passing; .ts.net acceptance is now gated by allowTailscale (which still defaults to true when tailscale.mode = \"serve\"), so reviewers should verify trustedProxies/allowRealIpFallback behavior and watch for cases where direct-proxy socket fallback changes other callers.", + "dedupe_summary": "Unique to Tailscale Serve 401: fallback resolveClientIp->socket address and allowRealIpFallback socket check, plus gating .ts.net host acceptance behind allowTailscale and threading that flag through auth callsites (depends on #50636)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the Tailscale Serve 401 root causes and precise code changes (.socket fallback, allowRealIpFallback check, .ts.net gating and flag threading) with no template noise, so it will cluster with related fixes." + }, + "usage": { + "input_tokens": 1652, + "output_tokens": 712 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "checkInboundAccessControl computes isSelfChat but never enforces it; when WhatsApp channel is configured without an explicit groupPolicy, resolveWhatsAppRuntimeGroupPolicy defaults to \"open\" so non-self messages (groups, strangers, broadcasts) bypass the self-chat isolation boundary and reach downstream processing.", + "solution_summary": "Add an early-return guard in checkInboundAccessControl that, when account.selfChatMode === true, blocks all non-self messages before any group/DM policy evaluation. The guard checks only the explicit selfChatMode flag (not the heuristic isSelfChat) and preserves self-messages via isSamePhone; isSelfChat is still returned for UI/logging.", + "maintainer_signal_summary": "All relevant unit tests passed (9 tests), code-path analysis and manual verification confirm guard placement before policy resolution, scope is limited to src/web/inbound/access-control.ts, and the change is intentionally narrow (explicit selfChatMode only); rollback is trivial by removing the early-return block.", + "dedupe_summary": "Distinct fix: enforces explicit selfChatMode hard-block in access-control.ts for WhatsApp to stop non-self messages; look for PRs touching groupPolicy defaults or auto-detection heuristics—this change is not a general groupPolicy/defaults change nor an auto-detect tweak and should not be merged as a duplicate of those. " + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary succinctly captures the core code change (hard-block selfChatMode in access-control.ts to stop non-self WhatsApp messages) but includes an unnecessary meta/instructional clause about merging/PR lookup that adds minor noise." + }, + "usage": { + "input_tokens": 1367, + "output_tokens": 608 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "With groupPolicy set to \"allowlist\", group messages from senders not in groupAllowFrom were completely dropped at the access-control layer and never reached the mention-gating/context-storage logic, so the agent could not see those messages as context.", + "solution_summary": "Introduces an opt-in config flag groupContextFromAll (top-level and per-account) that, when true, causes access-control to return storeForContext for non-allowlisted senders; those messages are passed through as contextOnly, stored in pending group history, do not trigger replies, and skip read receipts.", + "maintainer_signal_summary": "Change is opt-in and backward compatible (defaults false), per-account config takes precedence, and changes are localized to access-control → monitor → auto-reply flow; reviewers should watch allowlist normalization/compare logic and the fact that early context-only handling bypasses some existing group gating/state updates.", + "dedupe_summary": "Unique aspects: new groupContextFromAll flag, new access-control outcome storeForContext, contextOnly added to WebInboundMessage, threaded handling in monitor/on-message to record pending history without triggering replies; opt-in preserves existing behavior so duplicates should consider these structural changes rather than behavior regressions." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise and focused on the key code changes (new groupContextFromAll flag, storeForContext outcome, contextOnly field, pending-history handling) that implement storing non-allowlisted group messages as context; contains a small meta remark about duplicates but otherwise provides strong clustering signal." + }, + "usage": { + "input_tokens": 1384, + "output_tokens": 500 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "Add an optional sessionKey parameter to sessions_spawn so spawned sub-agents can reuse a deterministic session (preserve conversation history) instead of creating a new random UUID session each time. Short keys are namespaced to agent:{agentId}:subagent:{key}, fully-qualified keys are validated to prevent cross-agent session injection, and omission falls back to crypto.randomUUID().", + "solution_summary": "Implemented sessionKey handling, namespacing for short keys, validation of fully-qualified keys (reject if embedded agentId != targetAgentId), and fallback to random UUID; added tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch rejection. Also addresses a security finding about session key allowlist bypass. ", + "maintainer_signal_summary": "Reviewer flagged three blocking issues: a test file imports a non-existent module causing compile failure, a case-sensitivity bug in fully-qualified key validation (embedded agentId not normalized), and inconsistent error handling (throw vs jsonResult) for mismatch checks; labels indicate this touches agents and is size M. ", + "dedupe_summary": "Keywords: sessions_spawn sessionKey, deterministic sub-agent sessions, agent:{agentId}:subagent:{key}, fully-qualified key validation, cross-agent injection prevention, crypto.randomUUID fallback, test import error, case-sensitivity bug, inconsistent error handling." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Summary captures the core change (sessionKey for deterministic sub-agent sessions, mapping format, validation, and UUID fallback) but includes extra noisy items (test import error, case-sensitivity bug, inconsistent error handling) that weren't central and slightly dilute the signal." + }, + "usage": { + "input_tokens": 1437, + "output_tokens": 899 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) was using editMessageText, causing Telegram to return 400 \"there is no text in the message to edit\" and leaving edits failing/silent for captionable messages.", + "solution_summary": "editMessageTelegram now detects that specific 400 (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption for captionable messages; buttons-only updates use editMessageReplyMarkup directly; MESSAGE_NOT_MODIFIED is treated as success and HTML parse fallback is preserved.", + "maintainer_signal_summary": "Change is limited to the Telegram adapter (src/telegram/send.ts), backward compatible with no API/interface changes, and covered by unit tests for retry, buttons-only, error propagation, and MESSAGE_NOT_MODIFIED handling; risk is the regex failing if Telegram changes the error wording, but failures fall back to previous behavior.", + "dedupe_summary": "Unique fix: auto-detect captionable messages by catching the specific Telegram 400 and retrying with editMessageCaption, plus a direct reply-markup path; preserves public message tool interface and only touches the Telegram send implementation." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core bugfix (catch Telegram 400, retry with editMessageCaption, handle reply-markup separately) and notes scope-limitation without extraneous template or testing details." + }, + "usage": { + "input_tokens": 1318, + "output_tokens": 519 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "A prior change (#24962) hardcoded disableBlockStreaming: true for WhatsApp to stop reasoning-content leaks, which had the unintended effect of disabling block-streamed/progressive delivery entirely even when agents or per-account config enabled it.", + "solution_summary": "The deliver callback now only suppresses kind === \"tool\" payloads and disableBlockStreaming is read from channels.whatsapp.blockStreaming (falling back to agents.defaults.blockStreamingDefault), restoring config-driven block streaming while reasoning suppression remains handled upstream.", + "maintainer_signal_summary": "Author ran contract tests and live WhatsApp verification (progressive delivery confirmed), fixed failing tests, resolved bot review items, and left only reviewer/maintainer judgment items outstanding; change is backward compatible and releasable.", + "dedupe_summary": "Key identifiers: bug introduced by #24962 (hardcoded disableBlockStreaming), fix restores per-account/global blockStreaming config, reasoning/tool payload suppression behavior unchanged; keep if issue references those PR/issue numbers or the WhatsApp process-message change." + }, + "judge": { + "score": 4, + "has_boilerplate": true, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Captures the core bug and fix (hardcoded disableBlockStreaming from #24962 vs restoring per-account/global config and payload suppression behavior) but includes unnecessary meta/instructional text and PR references that are borderline boilerplate." + }, + "usage": { + "input_tokens": 1310, + "output_tokens": 566 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Telegram gateway hung when users sent videos larger than the configured mediaMaxMb (default 5MB). getFile returned OK but fetchRemoteMedia threw a content-length enforcement error leaving the response body unconsumed, which caused undici Agent.close() in closeDispatcher() to deadlock and make the gateway unresponsive.", + "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5MB to 20MB, added timeouts/retries to getFile and media downloads, made media download failures non-fatal so captions/text are still forwarded, and fixed closeDispatcher() to avoid Agent.close() deadlocks by timing out and calling destroy() as a fallback; also extended stripInboundMetadata to hide inbound media paths in the UI.", + "maintainer_signal_summary": "This is a tested bug fix and security hardening for the Telegram channel that prevents production outages; change is backward-compatible and configurable (mediaMaxMb), and should be merged promptly to restore reliability for bots receiving videos.", + "dedupe_summary": "Telegram integration hang due to unconsumed response body causing undici Agent.close deadlock when media exceeds maxBytes; fix: increase mediaMaxMb to 20, add download timeouts/retries, make media errors non-fatal, and add timeout+destroy fallback in closeDispatcher()." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that identifies the exact root cause (undici Agent.close deadlock from unconsumed response bodies when media exceeds maxBytes) and lists the targeted fixes, so it will cluster correctly." + }, + "usage": { + "input_tokens": 1406, + "output_tokens": 597 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "When Telegram users send document (PDF/DOCX) attachments with no caption, the code falls back to a hardcoded \"\" placeholder in resolveTelegramInboundBody, causing the agent to misidentify documents as images and mishandle them.", + "solution_summary": "Replace the hardcoded \"\" fallback with the existing type-aware placeholder variable so inbound body text correctly becomes \"\", \"\", etc.; add unit tests covering all media types and verify getFile is triggered for documents.", + "maintainer_signal_summary": "This is a small, backward-compatible bug fix scoped to agent-facing body text (closes #7116); media download and extraction logic are unchanged, unit tests were added (extensions/telegram/src/bot/helpers.test.ts and bot.create-telegram-bot.test.ts), and tests pass.", + "dedupe_summary": "Fix targets resolveTelegramInboundBody: replace hardcoded '' with the computed placeholder; key test files added to lock behavior for document/media placeholders and getFile invocation; change is limited to placeholder text and should be deduplicable by these file/method names and the ''→placeholder replacement." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summarizes the core change—replacing hardcoded '' in resolveTelegramInboundBody with a type-aware placeholder—though it includes minor noise about added tests and deduplication expectations that aren't strictly necessary." + }, + "usage": { + "input_tokens": 1233, + "output_tokens": 490 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "MEDIA_MAX_BYTES was hardcoded to 5 MB in src/media/store.ts, so files larger than 5 MB silently failed to stage into sandboxed containers and could not be processed by agents.", + "solution_summary": "Introduced a new optional config field media.maxBytes (bytes, max 500 MB) and replaced the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(); the gateway reads media.maxBytes on boot, error messages now report the configured limit, and the 5 MB default is preserved when unset.", + "maintainer_signal_summary": "All relevant unit tests pass; bot review threads were resolved (ensure set/reset behavior and MB formatting); change is backward compatible and reversible by removing the config; note risk of disk fill if users set the limit very high (schema enforces 500 MB cap).", + "dedupe_summary": "Make the media staging/store size limit configurable via media.maxBytes instead of a hardcoded 5 MB, with gateway applying the value on boot and a schema-enforced 500 MB maximum; default remains 5 MB." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states the core change (make 5 MB media staging limit configurable via media.maxBytes), how it's applied (gateway boot), that default remains, and notes the schema-enforced 500 MB cap, with no boilerplate or noise." + }, + "usage": { + "input_tokens": 1354, + "output_tokens": 647 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "When Telegram media download fails (due to exceeding mediaMaxMb or Telegram API errors) the handler returned early and the entire message—including caption text—was silently dropped, so the agent received no message or ack and the user got no feedback.", + "solution_summary": "On download failure the code now extracts file metadata (name, size, type) from the Telegram message object, injects a metadata stub into msg.text (e.g. \"⚠️ [File: name, 40MB] (exceeds 5MB)\"), and lets the message continue through the normal pipeline instead of returning early.", + "maintainer_signal_summary": "This is a bug fix in the Telegram integration (src/telegram/bot-handlers.ts), backward-compatible and limited to the error path; it removes the prior automatic reply-to-user messages and notes a small risk from mutating msg.text but cites easy revert by reverting the commit.", + "dedupe_summary": "Specifically fixes silent drops on media download errors and size-limit cases (mediaMaxMb and Telegram getFile limit); does not change normal download paths, media group handling, sticker processing, or outbound file sending, so it's distinct from general media handling refactors." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states the core fix (preventing silent drops on Telegram media download or size-limit failures) using relevant technical terms and clarifies scope, so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 1196, + "output_tokens": 484 + } + }, + { + "thread_id": 5796, + "number": 24117, + "kind": "pull_request", + "title": "feat(cli): add --session-key targeting for openclaw agent", + "summary": { + "problem_summary": "The CLI lacks a --session-key option, so CLI-triggered deliveries in multi-channel/multi-agent setups can resolve to an agent main session instead of a specific channel because sessionId is UUID-based and not externally discoverable and --to may be unavailable in callback contexts.", + "solution_summary": "Adds a first-class --session-key flag to openclaw agent (register.agent.ts), extends AgentCliOpts with sessionKey, forwards it into resolveSessionKeyForRequest in src/commands/agent-via-gateway.ts, preserves resolution precedence (session-key → session-id → --to → agent) and updates help/validation and tests.", + "maintainer_signal_summary": "Change is additive and backward-compatible, tests for CLI parsing and gateway forwarding were added and passing, and the change is low-risk and scoped to CLI option parsing and session resolution pass-through.", + "dedupe_summary": "Key identifiers: --session-key flag, resolveSessionKeyForRequest, AgentCliOpts, agent-via-gateway, register.agent.ts; duplicates should match PRs adding a sessionKey CLI option or forwarding sessionKey through gateway with same precedence semantics." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: names the new --session-key flag, related functions/files, and the exact duplicate-match criteria, so it captures the core change without noise." + }, + "usage": { + "input_tokens": 1168, + "output_tokens": 479 + } + }, + { + "thread_id": 766, + "number": 11611, + "kind": "pull_request", + "title": "feat: separate group-level allowlist from sender-level command authorization", + "summary": { + "problem_summary": "With groupPolicy: \"allowlist\" the code currently gates all group interactions by sender phone number (groupAllowFrom), preventing: (1) allowlisting groups by JID while letting any participant chat, (2) restricting slash commands to owner only, and (3) blocking spam groups while keeping approved groups open.", + "solution_summary": "When groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access is gated by the groups JID allowlist (via resolveChannelGroupPolicy) so only listed groups are accepted and anyone in an accepted group can chat; groupAllowFrom continues to control slash-command authorization. If no groups mapping is present, existing sender-based filtering is preserved. Code changes touch src/web/inbound/access-control.ts, JSDoc for WhatsApp config, and add unit tests; depends on #3326.", + "maintainer_signal_summary": "Change appears localized and safe (confidence ~4/5). Tests were added to cover the new branches and docs updated; one outdated inline comment was flagged as a minor doc/cleanup item. Reviewers should note the dependency on #3326 and the intended behavior change when a groups mapping is present.", + "dedupe_summary": "Unique change: introduce a group-JID allowlist that supersedes sender-based gating when channels.whatsapp.groups exists, while keeping groupAllowFrom dedicated to command authorization; preserves legacy behavior when groups is absent. Files: access-control.ts, config types, and new tests; depends on PR #3326." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Clearly states the core change—adding a group-JID allowlist that supersedes sender-based gating while retaining groupAllowFrom for command authorization and preserving legacy behavior—though it includes minor noise about files/PR dependency." + }, + "usage": { + "input_tokens": 1218, + "output_tokens": 614 + } + }, + { + "thread_id": 17924, + "number": 46425, + "kind": "pull_request", + "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", + "summary": { + "problem_summary": "Configured mediaMaxMb is ignored for Telegram outbound reply messages because loadWebMedia in the reply delivery path never receives a maxBytes parameter and falls back to the 100 MB default.", + "solution_summary": "Thread mediaMaxBytes through the Telegram outbound delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia receives the configured limit; added two unit tests to verify behavior.", + "maintainer_signal_summary": "All related tests pass (108 total across delivery, dispatch, native-commands, bot-message); change is backward-compatible when mediaMaxBytes is unset, does not alter direct-send behavior or media internals, and closes #46023.", + "dedupe_summary": "Propagate mediaMaxBytes to reply delivery so Telegram outbound replies obey mediaMaxMb; small, low-risk parameter-threading bugfix with tests and no behavioral change when unset." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that names the exact parameter and affected path (Telegram outbound reply delivery) and notes it's a low-risk bugfix, so it will cluster well with similar fixes." + }, + "usage": { + "input_tokens": 1219, + "output_tokens": 570 + } + }, + { + "thread_id": 5712, + "number": 37050, + "kind": "pull_request", + "title": "fix(telegram): add HTML parse fallback for media captions", + "summary": { + "problem_summary": "Telegram media sends (photo, document, video, animation, audio, voice, video note) failed with a 400 \"can't parse entities\" error when captions contained invalid HTML entities because only text messages used an HTML-parse fallback; the media path had no retry and the entire send would fail.", + "solution_summary": "Wrapped the media send callback in withTelegramHtmlParseFallback so on an HTML parse error the code retries the send without parse_mode and with the raw caption (plain text). This change applies to all media types and includes a unit test that mocks sendPhoto rejecting then succeeding to verify the retry behavior.", + "maintainer_signal_summary": "Bug fix only, backward-compatible, unit test added; change limited to src/telegram/send.ts (media send inner callback) and does not affect text/edit/thread/caption-splitting logic. Can be reverted by restoring the original sendMedia implementation if needed.", + "dedupe_summary": "Add HTML-parse fallback for Telegram media captions: retry media send without parse_mode on \"can't parse entities\" 400; previously only text messages retried; unit test added; change in src/telegram/send.ts." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core change—adding a retry fallback for Telegram media captions on HTML-parse errors—without extraneous template text, so it would cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 1093, + "output_tokens": 486 + } + }, + { + "thread_id": 21769, + "number": 55280, + "kind": "pull_request", + "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", + "summary": { + "problem_summary": "WhatsApp had block streaming effectively disabled (disableBlockStreaming hardcoded true) and a race in the block-reply pipeline: block replies were enqueued optimistically and the pipeline marked content as sent before actual delivery, causing “silent turns” where neither intermediate blocks nor final reply reached WhatsApp.", + "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming, add sensible coalescing defaults, fix activation logic (remove undefined fallthrough), introduce sendBlockReplyAsync that resolves/rejects on real delivery, and await it in onBlockReply so sentContentKeys is set only after confirmed delivery. Tests and mocks were updated accordingly.", + "maintainer_signal_summary": "This PR fixes the root cause of the silent-turn bug reverted in #24962, restores configurable block streaming consistent with other channels, and includes unit and manual verification; reviewers should focus on process-message.ts, reply-dispatcher.ts, dispatch-from-config.ts, and the activation logic change.", + "dedupe_summary": "Keywords/unique points: WhatsApp blockStreaming hardcoded true, disableBlockStreaming activation bug (undefined vs false), delivery race marking sentContentKeys too early, sendBlockReplyAsync added, block-streaming coalesce defaults, modified files process-message.ts/shared.ts/reply-dispatcher.ts/dispatch-from-config.ts; fixes silent turns and enables per-account/global channels.whatsapp.blockStreaming." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that highlights the root cause (hardcoded disableBlockStreaming and delivery race), the configurable change (per-account/global channels.whatsapp.blockStreaming), and key code fixes (sendBlockReplyAsync, sentContentKeys timing) without template noise." + }, + "usage": { + "input_tokens": 1006, + "output_tokens": 656 + } + }, + { + "thread_id": 8098, + "number": 25543, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked per-group allowlist support—only sender-level groupAllowFrom existed—so specific Signal groups couldn't be explicitly allowed to bypass sender checks or run control commands.", + "solution_summary": "Introduces channels.signal.groups. with a new SignalGroupConfig and Zod schema, wires resolveChannelGroupPolicy into the Signal event handler and command authorizers so listed groups bypass sender-level checks, and updates docs.", + "maintainer_signal_summary": "Reviewer (Greptile) endorsed changes as safe to merge (confidence 5/5); TypeScript compilation passes, manually tested, addresses prior PR feedback (commandGate integration) and maintains backwards compatibility via hasGroupAllowFrom.", + "dedupe_summary": "Key identifiers: config path channels.signal.groups., types SignalGroupConfig/SignalAccountConfig, schema SignalGroupSchema/SignalAccountSchemaBase, function resolveChannelGroupPolicy, commandGate authorizers, modified files src/config/types.signal.ts, src/config/zod-schema.providers-core.ts, src/signal/monitor/event-handler.ts, docs/channels/signal.md; fixes #25540, supersedes #4337." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that names the added config (channels.signal.groups.), related types/schemas, handler function, and touched files—providing the exact technical signals needed to cluster similar changes." + }, + "usage": { + "input_tokens": 948, + "output_tokens": 681 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal lacked group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs the access check treated those as sender IDs (phone/UUID) and always failed, causing group messages to be silently dropped.", + "solution_summary": "Added a groups config to SignalAccountSchemaBase (same shape as other channels) and invoked resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom; added four tests to cover Signal group-level policy resolution.", + "maintainer_signal_summary": "Small, low-risk change that mirrors other channels: schema addition + access-check update; includes tests and claims to fix #25540 with all relevant tests passing.", + "dedupe_summary": "Detect duplicates by the combination of keywords: \"SignalAccountSchemaBase groups\", \"resolveChannelGroupPolicy\", \"group-level allowlisting\", \"groupAllowFrom mismatch\", and issue #25540." + }, + "judge": { + "score": 2, + "has_boilerplate": true, + "signal_density": 2, + "would_cluster_correctly": true, + "reasoning": "The entry includes useful technical keywords but is framed as a detection instruction and omits the core change (Signal lacked group-level allowlisting and the fix adds a groups config to SignalAccountSchemaBase), so it's noisy and incomplete for deduplication." + }, + "usage": { + "input_tokens": 748, + "output_tokens": 638 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "WhatsApp delivery currently batches all intermediate assistant text into a single message because process-message.ts hardcodes disableBlockStreaming: true, so tool-call/sleep separated blocks are not sent as separate messages.", + "solution_summary": "Respect channels.whatsapp.blockStreaming by adding resolveWhatsAppBlockStreamingEnabled() and replacing the hardcoded flag, register blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in dock, and treat intermediate block payloads as delivered when streaming is enabled; tests updated accordingly.", + "maintainer_signal_summary": "Labels: channel: whatsapp-web, app: web-ui, agents, size: S. Tests updated (process-message.inbound-contract.test.ts) to match other channels; all 10 tests pass and anyReplyDelivered verifies intermediate block delivery. Change is backwards-compatible when blockStreaming is false or omitted.", + "dedupe_summary": "Enable config-driven WhatsApp block streaming (channels.whatsapp.blockStreaming) so intermediate blocks can be delivered separately instead of being hardcoded to batch; includes code changes, dock registration, and updated tests." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change—making WhatsApp block streaming configurable so intermediate blocks are delivered—while only slightly including minor implementation/test notes that don't harm deduplication." + }, + "usage": { + "input_tokens": 498, + "output_tokens": 493 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v2-signal-focused.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v2-signal-focused.json new file mode 100644 index 0000000..38a7542 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v2-signal-focused.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v2-signal-focused", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "You are extracting the core signal from a GitHub issue or pull request for duplicate detection. The output will be embedded and clustered with similar issues.\n\nYour job is to answer: \"What is this issue/PR fundamentally about?\" Ignore everything else.\n\nStrip away ALL of the following — they add noise to embeddings:\n- Template boilerplate (headings like \"### Steps to reproduce\", \"### Expected behavior\")\n- Testing notes, QA instructions, review checklists\n- Environment details (OS, browser, versions) unless they ARE the core issue\n- Deployment procedures, rollback plans\n- Links, references to other issues\n- Formatting artifacts (checkboxes, horizontal rules, badges)\n\nWhat to capture:\n- The specific bug, feature, or change in concrete terms\n- Which component/module/API/subsystem is affected\n- Why it matters (the user impact, not the testing impact)\n\nReturn JSON with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nRules: plain text only, no markdown, 1-3 sentences each.\ndedupe_summary is the most important field — it must be a clean, specific description that would match other reports about the same underlying issue.", + "timestamp": "2026-03-30T16:29:16.007Z", + "aggregate": { + "avg_score": 4.97, + "avg_signal_density": 4.97, + "boilerplate_count": 0, + "boilerplate_pct": 0, + "would_cluster_correctly_pct": 100, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 47983, + "total_output_tokens": 25794 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "channels.discord.proxy only applies to the Discord WebSocket Gateway; REST API calls (command deploy, fetch bot identity) and guild resolution do not use the configured HTTP proxy, causing startup REST timeouts and unresolved guild channels so the bot is unresponsive in guilds while DMs work.", + "solution_summary": "Apply the configured proxy to all HTTP/REST requests (set undici's ProxyAgent/global dispatcher for REST and guild-resolve code paths) or add a global proxy configuration that installs a ProxyAgent for all network calls; ensure guild resolve uses the proxied path and add retry logic.", + "maintainer_signal_summary": "Regression: worked before but now Node.js built-in fetch/undici doesn't respect HTTP_PROXY, and current code only proxies the Gateway WS. A workaround (setGlobalDispatcher) fixes REST but not guild resolve, indicating a separate code path; tests and fixes should target REST/guild-resolve startup paths and consider a global proxy option.", + "dedupe_summary": "Discord channel proxy config only proxies the WebSocket gateway, not REST API calls or guild resolution, causing REST timeouts and unresolved guild channels behind an HTTP proxy." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (proxy applies to WebSocket but not REST/guild resolution) without boilerplate or irrelevant details, so it will cluster with similar reports." + }, + "usage": { + "input_tokens": 2335, + "output_tokens": 745 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "Outbound Discord REST requests (message sends/attachments) ignore the configured channels.discord.proxy and fail with TypeError: fetch failed, while the WebSocket gateway (receiving) works. The Carbon RequestClient/fetch implementation currently has no proxy support, so bots behind network proxies (e.g., China) cannot send messages or upload/download media.", + "solution_summary": "Add proxy support to the Discord REST client by making RequestClient/RequestHandler pass a proxy agent (HTTP/SOCKS/TUN) or honor channels.discord.proxy for all outgoing HTTP requests, including message sends and attachment uploads/downloads. Ensure REST fetches use the same proxy behavior as the gateway and provide config-driven proxy handling.", + "maintainer_signal_summary": "User reports logs indicate \"rest proxy enabled\" but REST requests still bypass the proxy; WebSocket receives function normally. Affected OpenClaw version 2026.2.26; change required in Carbon's RequestClient/fetch integration to attach a proxy agent or use a proxy-aware fetch implementation so REST traffic routes through the configured proxy.", + "dedupe_summary": "Discord REST requests bypass the configured channels.discord.proxy because the Carbon RequestClient/fetch has no proxy support, causing outgoing message sends and attachment transfers to fail behind proxies." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "The summary succinctly states the core bug and root cause with precise technical terms (channels.discord.proxy, Carbon RequestClient/fetch, REST requests) and no extraneous template or testing details, so it will cluster with similar proxy-related REST failures." + }, + "usage": { + "input_tokens": 1017, + "output_tokens": 747 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "When OpenClaw's Discord provider is configured to use an HTTP proxy it can successfully connect the gateway/WebSocket and fetch the application ID, but startup REST calls (native command deployment and bot identity fetch) consistently fail with 'fetch failed', leaving the provider half-working.", + "solution_summary": "Ensure all Discord REST requests use the same proxy-aware HTTP fetch/dispatcher as the gateway (unify transport/proxy handling), so startup REST calls are routed through the configured proxy and include consistent retry/error handling.", + "maintainer_signal_summary": "Logs show 'rest proxy enabled' and 'gateway proxy enabled', application ID fetch succeeds, but deploy-rest:put and fetch-bot-identity return 'fetch failed' despite 'logged in to discord'. Issue reproducible with channels.discord.proxy configured; OpenClaw versions 2026.3.12/2026.3.13 reported. Likely different code paths for gateway vs REST requests.", + "dedupe_summary": "Discord provider connects via proxy to the gateway but some startup REST calls (PUT /applications/.../commands and bot identity fetch) fail with 'fetch failed' — likely due to inconsistent or missing proxy handling for certain REST request paths." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that identifies the core failure (startup REST calls for command deployment and bot identity fetch failing with 'fetch failed' despite gateway login) and points to a likely cause (inconsistent/missing proxy handling) which is ideal for deduplication." + }, + "usage": { + "input_tokens": 1163, + "output_tokens": 713 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "Discord channel proxy is logged as enabled but Discord REST API calls fail with \"fetch failed\" and the gateway WebSocket never connects when running OpenClaw under WSL2 with an HTTP proxy configured (channels.discord.proxy). This renders the Discord channel non-functional despite the proxy being reachable via curl.", + "solution_summary": "Ensure the configured HTTP proxy is actually applied to the Discord.js REST/fetch client and WebSocket gateway (use a proxy agent or set fetch/HTTP client to honor channels.discord.proxy), so REST requests and gateway connections go through the proxy in WSL2 and similar environments.", + "maintainer_signal_summary": "Logs show \"rest proxy enabled\" and \"gateway proxy enabled\" but subsequent REST operations error with \"fetch failed\" and gatewayConnected=false; curl via the proxy succeeds and Feishu works, so the proxy itself is fine. Affects users requiring HTTP proxy (WSL2); reproducible and high severity; config stored at ~/.openclaw/openclaw.json → channels.discord.proxy.", + "dedupe_summary": "Discord proxy config is recognized but not actually applied to the Discord.js REST/fetch (and gateway) calls, causing \"fetch failed\" and gateway connection failures when an HTTP proxy is required (e.g., WSL2)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core problem — proxy config is detected but not applied to Discord.js REST/gateway calls causing 'fetch failed' — without extraneous template or repro details." + }, + "usage": { + "input_tokens": 1000, + "output_tokens": 525 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "Discord REST API calls from the OpenClaw gateway ignore channels.discord.proxy and HTTP(S)_PROXY environment variables and fail with 'TypeError: fetch failed', while the Discord WebSocket and other channels (e.g., Telegram) work correctly.", + "solution_summary": "Route Discord REST API requests through the same proxy-aware fetch/SSRF-guard path used for web_search/web_fetch so channels.discord.proxy and HTTP(S)_PROXY are honored; update the Discord channel code to use the shared proxy-aware fetch implementation.", + "maintainer_signal_summary": "Logs show 'discord final reply failed: TypeError: fetch failed'. User provided config, env vars, and reproduction steps; curl through the proxy to Discord API succeeds, and changelog notes web tools were fixed but Discord REST was excluded. User attempted both channel config and env vars and reports a webhook workaround only for push, not replies.", + "dedupe_summary": "OpenClaw's Discord REST API requests are not routed through the configured proxy/HTTP(S)_PROXY and fail with 'fetch failed'." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary stating that Discord REST requests aren't using the configured proxy (HTTP(S)_PROXY) and fail with 'fetch failed', which captures the core bug for deduplication." + }, + "usage": { + "input_tokens": 1023, + "output_tokens": 701 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "The CLI command openclaw agent --channel discord --to resolves and sends messages to the generic agent:*:main session instead of the live Discord channel/thread session because the session resolver does not look up sessions by delivery context (channel + to + accountId) and Discord targets are not normalized.", + "solution_summary": "Update the agent CLI session resolution to search by real delivery context (channel + to + accountId), normalize Discord target identifiers (e.g., prepend channel:), ensure the command path passes channel and replyAccount/accountId to the resolver, and fix compact resolver lookup logic to use the correct main-session builder.", + "maintainer_signal_summary": "Manual hotfixes were applied to compiled dist files (dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js) to add delivery-context lookup and normalization; changes are high-risk and will be lost on reinstall. Commit an equivalent fix in source, run post-update revalidation, and review the compact resolver and command-path propagation to avoid regressions.", + "dedupe_summary": "openclaw CLI agent --channel discord --to routes to agent:*:main instead of the live Discord channel/thread because the session resolver omits lookup by delivery context (channel+to+accountId) and Discord target normalization is missing." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that states the CLI command, observed misrouting to agent:*:main, and the precise root causes (missing delivery-context lookup and Discord target normalization), which is ideal for deduplication." + }, + "usage": { + "input_tokens": 950, + "output_tokens": 507 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions in OpenClaw's Discord integration: 1) volatile per-message metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) was being re-injected into thread sessions every turn, causing progressive context bloat and degraded behavior; 2) CLI agent commands targeted to a Discord thread routed to the default main session (agent:*:main) because the session resolver did not lookup by actual delivery context (channel+to+accountId).", + "solution_summary": "Prevent re-injection of specified volatile metadata for Discord threads; update CLI/gateway session resolution to normalize Discord targets and resolve sessions by delivery context (channel + to + accountId); pass channel and replyAccount/accountId into the resolver and replace incorrect resolver usage with buildAgentMainSessionKey(...).", + "maintainer_signal_summary": "Fixes were applied directly to compiled runtime bundles in openclaw/dist/ (pi-embedded, gateway-cli, compact, register.agent) and verified by restarting the gateway service and observing corrected writer/reasoning sessions and no repeated thread context; manual patching of dist/ files is required and will be lost on reinstall, so re-application script/update is needed.", + "dedupe_summary": "Discord thread sessions repeatedly receive volatile metadata each turn (message_id, reply_to_id, timestamp, Thread starter, chat-history) causing context bloat, and CLI agent commands route to the wrong session because the resolver doesn't resolve by delivery context (channel+to+accountId)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: captures the two core regressions (repeated injection of volatile Discord thread metadata causing context bloat, and CLI routing to wrong session due to resolver not using delivery context), with no boilerplate or irrelevant detail." + }, + "usage": { + "input_tokens": 869, + "output_tokens": 528 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "The sandbox media staging limit is hardcoded as MEDIA_MAX_BYTES = 5 * 1024 * 1024 inside dist/store-*.js, causing any files over 5MB to be rejected or silently fail when copying into sandboxed agents (sandbox.mode: \"non-main\").", + "solution_summary": "Make the MEDIA_MAX_BYTES value configurable via openclaw.json (e.g., tools.media.maxBytes) or honor existing per-type media limits before staging; remove the hardcoded constant in the distributed build and read the limit from runtime config with a sensible default.", + "maintainer_signal_summary": "Regression present since at least v2026.3.7 and still in v2026.3.8; users currently must patch dist/store-*.js after npm install to raise the cap (sed workaround). Affects npm-installed distribution and causes silent failures for >5MB files in sandboxed agents.", + "dedupe_summary": "MEDIA_MAX_BYTES hardcoded to 5MB in dist/store-*.js prevents staging files >5MB into sandboxed agents and there is no openclaw.json config to override it." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "The summary concisely and specifically states the core bug (hardcoded MEDIA_MAX_BYTES = 5MB in dist/store-*.js blocking sandbox file staging) and that there's no openclaw.json override, which is ideal for deduplication." + }, + "usage": { + "input_tokens": 916, + "output_tokens": 626 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "OpenClaw's Discord provider fails all REST API requests with \"fetch failed\" even though the REST proxy is configured and logged as enabled; the Gateway WebSocket works and the bot can receive messages but cannot send replies or deploy commands.", + "solution_summary": "Update the Discord REST client to actually use the configured proxy for fetch calls (e.g., attach an HTTP(S) proxy agent or honor proxy env vars) so REST requests are routed through the proxy like curl and the Gateway are.", + "maintainer_signal_summary": "Logs show \"rest proxy enabled\" and gateway proxy enabled, WebSocket connection and inbound messages work, and direct curl with the same proxy succeeds, but native command deployment and other REST calls fail with \"fetch failed\"; indicates the REST proxy implementation or fetch usage isn't honoring the proxy configuration.", + "dedupe_summary": "Discord REST API requests error with \"fetch failed\" despite rest proxy being enabled in OpenClaw; WebSocket works and curl with the proxy succeeds, implying the Discord provider's REST client isn't using the configured proxy and prevents sending messages." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies REST API calls failing with 'fetch failed' despite proxy config, notes WebSocket/curl workarounds, and pinpoints the likely cause (Discord REST client not using the proxy), which is ideal for deduplication." + }, + "usage": { + "input_tokens": 861, + "output_tokens": 751 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "Discord REST API calls made via @buape/carbon's RequestClient ignore the channels.discord.proxy setting in openclaw.json and call globalThis.fetch without a proxy-aware dispatcher; as a result REST requests (and other fetch paths like memorySearch and attachment downloads) bypass the configured HTTP proxy while the WebSocket gateway uses it.", + "solution_summary": "Change @buape/carbon's RequestClient (and other fetch code paths) to accept and use an explicit dispatcher/proxy option and have OpenClaw pass channels.discord.proxy into those REST/fetch calls, or set a proxy-aware undici dispatcher for native fetch so Node's fetch routes through the configured proxy.", + "maintainer_signal_summary": "Update RequestClient API to accept a dispatcher/proxy and wire OpenClaw channels.discord.proxy into REST calls; audit memorySearch and fetchRemoteMedia to use the same proxy handling; avoid using NODE_OPTIONS=--use-env-proxy as it breaks WebSocket connections.", + "dedupe_summary": "Discord REST API requests via @buape/carbon RequestClient bypass the configured channels.discord.proxy (globalThis.fetch used without proxy dispatcher), so REST/fetch traffic is not routed through the HTTP proxy while the WebSocket gateway is." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies @buape/carbon RequestClient using globalThis.fetch and bypassing channels.discord.proxy so REST calls aren't routed through the HTTP proxy, matching similar reports accurately." + }, + "usage": { + "input_tokens": 870, + "output_tokens": 781 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "OpenClaw degrades under sustained load with long-running conversations: context grows without bounds (writer ~42k tokens, reasoning ~90k) causing rising per-turn latency; long turns block Discord lanes and cause serialization under load; typing indicators can remain stuck until TTL in some code paths.", + "solution_summary": "Applied configuration limits and timeouts: reduced concurrency (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents maxConcurrent 8→4, maxChildrenPerAgent 8→4), set agents.defaults.contextTokens=80000, increased channels.discord.eventQueue.listenerTimeout 120000→300000, improved patching script to handle hash variants, and restarted the gateway.", + "maintainer_signal_summary": "Root causes are missing effective context token limits, overly high concurrency saturating lanes, and insufficient event queue timeouts; restart mitigates but does not eliminate the problem. Recommended long-term fixes: enforce/adapt context token caps, tighten concurrency controls, increase event-queue timeouts, and expose these as tunable config options.", + "dedupe_summary": "Performance degradation and Discord lane blocking during sustained long conversations caused by unbounded context token growth and excessive concurrency, leading to serialization and stuck typing indicators; remedy via context token limits, lower concurrency, and longer event-queue listener timeouts." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that names the root technical causes (unbounded context token growth, excessive concurrency) and observable impacts (Discord lane blocking, serialization, stuck typing), with suggested mitigations — no boilerplate and good for clustering similar issues." + }, + "usage": { + "input_tokens": 804, + "output_tokens": 862 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11, Discord thread handling repeatedly injects per-message volatile metadata (message_id, reply_to_id, timestamp, Thread starter, \"Chat history since last reply\") on every conversation turn, causing context contamination, token bloat, performance degradation, and prompt cache invalidation.", + "solution_summary": "Add an isNewSession guard so thread-starter/history and volatile IDs/timestamps are injected only on the first turn, correct session key generation to use buildAgentMainSessionKey(...), and implement the fix in source (not just compiled dist) with post-update validation.", + "maintainer_signal_summary": "A manual hotpatch was applied to compiled files (dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js) adding an isNewSession guard and replacing resolveAgentMainSessionKey with buildAgentMainSessionKey; this is high-risk because the changes live only in dist and will be lost on updates, so a source-level fix and automated revalidation are required.", + "dedupe_summary": "Discord threads re-inject per-message volatile metadata every turn (message_id, reply_to_id, timestamp, Thread starter, chat history) due to a missing isNewSession guard, causing context pollution and prompt cache busting." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that identifies the exact symptom (per-turn injection of volatile Discord thread metadata), root cause (missing isNewSession guard), and impact (context pollution and cache busting), making it ideal for deduplication." + }, + "usage": { + "input_tokens": 816, + "output_tokens": 829 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "Discord thread messages repeatedly have volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) re-injected on every turn, causing context duplication, degraded performance in long threads, increased token costs, and prompt-cache busting. The issue originates from the compiled runtime handling of Discord threads in dist/pi-embedded-D6PpOsxP.js which lacks a guard to only inject that metadata on the first turn.", + "solution_summary": "A manual patch to dist/pi-embedded-D6PpOsxP.js was applied to stop injecting volatile Discord thread metadata after the first turn, restoring prompt caching and reducing token usage; recommended permanent fix is to implement the guard in source code and add revalidation post-update. ", + "maintainer_signal_summary": "High-risk regression: current fix is a manual edit to compiled dist files and will be lost on updates; maintainers should apply the fix in source, add a test/guard to ensure metadata injection happens only on the first thread turn, and include this in CI to prevent future regressions. ", + "dedupe_summary": "Discord threads repeatedly re-inject per-message volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, duplicating context and busting prompt cache due to missing first-turn injection guard in the compiled runtime (dist/pi-embedded-*.js)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that names the exact metadata re-injected, the root cause (missing first-turn guard), and the compiled runtime file pattern, so it will accurately cluster similar reports." + }, + "usage": { + "input_tokens": 824, + "output_tokens": 637 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "Discord threads in the OpenClaw runtime exhibit two regressions: per-turn re-injection of message metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing progressive context growth and latency, and the session resolver falling back to agent:*:main instead of resolving an existing live thread-bound session, causing CLI commands to route to the wrong session.", + "solution_summary": "Applied runtime patches to compiled dist JS bundles to stop repeated metadata reinjection and to make session resolution use delivery context (channel + to + accountId); added idempotent re-apply script and backups, restarted and validated the gateway.", + "maintainer_signal_summary": "Fix was applied manually to compiled bundles (dist/*.js) and is non-persistent across installs—backups (*.bak-*) and an idempotent reapplication script exist; gateway restarted and logs verified; upstream permanent fix and regression tests should be implemented to avoid repeated manual patches.", + "dedupe_summary": "Discord-thread regression: per-turn repeated metadata injection causing unbounded context growth and latency, plus session resolver falling back to agent:*:main instead of resolving thread-bound sessions, resulting in wrong CLI/session routing." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it names the two technical regressions (per-turn metadata causing context growth/latency and session resolver falling back to agent:*:main instead of thread-bound sessions) with no boilerplate, so it will cluster correctly." + }, + "usage": { + "input_tokens": 800, + "output_tokens": 715 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "OpenClaw cannot send reply messages to Discord: sending fails with \"TypeError: fetch failed\" even though Discord proxy settings are enabled and the bot is logged in; receiving messages and model responses work fine.", + "solution_summary": "Ensure the Discord REST send-message path respects the configured proxy (use a proxy agent or honor HTTP_PROXY/HTTPS_PROXY for Node fetch) and surface the underlying fetch error; update fetch usage to attach the proxy agent or global proxy settings so replies route through the proxy.", + "maintainer_signal_summary": "Regression observed in 2026.3.2: logs show \"discord: rest proxy enabled\" and \"discord: gateway proxy enabled\" and successful login, but final reply fails with \"TypeError: fetch failed\"; curl through the same proxy succeeds, and the issue affects both DMs and group chats, indicating the REST send code likely isn't applying the proxy.", + "dedupe_summary": "OpenClaw cannot send Discord reply messages because the REST send path fails with \"fetch failed\" despite proxy being enabled and working elsewhere, indicating the message-send fetch call is not using the configured proxy." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that identifies the core failure (Discord REST send path failing with 'fetch failed' despite proxy config) and the likely cause (send call not using the proxy), with no template noise." + }, + "usage": { + "input_tokens": 740, + "output_tokens": 686 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "openclaw agent --channel discord --to ... routes messages to agent:*:main instead of the live Discord channel/thread session because the CLI session resolver does not lookup sessions by delivery context and does not normalize Discord target IDs.", + "solution_summary": "Modify the session resolver to normalize Discord targets (e.g., prepend channel:) and perform a session lookup by delivery context (channel + to + accountId) before falling back to agent:*:main; the reporter patched the compiled dist/gateway-cli-BjsM6fWb.js manually with this fix.", + "maintainer_signal_summary": "The bug is in the compiled artifact dist/gateway-cli-BjsM6fWb.js; a manual edit was made to dist to add normalization and delivery-context lookup, but this is high-regression risk and will be lost on reinstall—fix must be applied in source and released properly.", + "dedupe_summary": "CLI agent routing to Discord channels falls back to agent:*:main because the session resolver fails to normalize Discord target IDs and doesn't lookup sessions by delivery context (channel+to+accountId)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states the CLI routes to agent:*:main because the session resolver doesn't normalize Discord target IDs or lookup by delivery context (channel+to+accountId)." + }, + "usage": { + "input_tokens": 805, + "output_tokens": 708 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "A regression in OpenClaw v2026.3.11: the compiled file dist/compact-1mmJ_KWL.js calls a nonexistent function resolveAgentMainSessionKey, causing ReferenceError at runtime, breaking the agent command and potentially crashing the gateway when processing agent commands (session resolver patch introduced the wrong function).", + "solution_summary": "Manually patch dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...); this removes the ReferenceError and restores agent/session behavior. Note this is a high-risk fix because it modifies compiled files and will be lost on reinstall unless fixed in source. ", + "maintainer_signal_summary": "This is a regression introduced by the earlier session resolver fix (#3); maintainers should update the source patch to use buildAgentMainSessionKey instead of resolveAgentMainSessionKey so compiled bundles do not contain the invalid call. The issue is urgent because the manual fix in dist/ will be overwritten by updates. ", + "dedupe_summary": "Compiled bundle dist/compact-1mmJ_KWL.js calls undefined resolveAgentMainSessionKey instead of buildAgentMainSessionKey, causing ReferenceError and breaking agent commands in OpenClaw v2026.3.11; requires replacing the call in the compiled file or fixing the source patch." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary clearly states the concrete regression (wrong function call in compiled bundle causing ReferenceError and broken agent commands) but includes a version tag and a remediation note which are minor noise for deduping." + }, + "usage": { + "input_tokens": 776, + "output_tokens": 608 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "Discord plugin's REST API requests ignore the configured channels.discord.proxy (only WebSocket connections use it), causing health-checks to report 'fetch failed' and message sends to time out in environments that require an HTTP proxy.", + "solution_summary": "Ensure the Discord plugin's HTTP client uses channels.discord.proxy for all REST API requests and health checks so REST calls are routed through the configured proxy; add tests and update health-check logic to use the proxied client.", + "maintainer_signal_summary": "Regression observed in OpenClaw 2026.3.8 (commit 3caab92). Repro: set channels.discord.proxy, verify curl via proxy succeeds, then run openclaw message send or health check which fails with 'fetch failed' while WebSocket proxying works—indicating REST calls bypass the proxy.", + "dedupe_summary": "Discord channel REST API requests ignore channels.discord.proxy (only WebSocket uses the proxy), causing fetch failed/timeouts behind an HTTP proxy." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and focused: it states the core bug (REST API ignores channels.discord.proxy while WebSocket uses it) with technical terms and impact (fetch failures/timeouts), no boilerplate or extraneous detail." + }, + "usage": { + "input_tokens": 732, + "output_tokens": 724 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the compiled session resolver for the agent command (dist/compact-1mmJ_KWL.js) does not perform a delivery-context lookup (channel + to + accountId), so sessions fall back to agent:*:main instead of resolving to the live session for the target channel/thread. This breaks routing and causes agent responses to be associated with the wrong session.", + "solution_summary": "Implement delivery-context lookup in the agent command's session resolver: search for an existing session by channel + to + accountId and only fall back to agent:*:main when no specific session is found; apply the change in source code and rebuild (not just edit compiled dist files), and add tests to cover delivery-context resolution.", + "maintainer_signal_summary": "This is a production-impact bug in compiled code; the correct fix must be applied in the source and included in the build pipeline because manual edits to dist/compact-*.js are high-risk and will be overwritten by upgrades. Prioritize a source-level fix, add tests, and release a rebuild to prevent regressions.", + "dedupe_summary": "Agent command session resolver (dist/compact-*.js) lacks delivery-context lookup (channel+to+accountId), causing sessions to default to agent:*:main instead of the target channel/thread session." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies the session resolver in dist/compact-*.js missing delivery-context lookup (channel+to+accountId) and the resulting incorrect fallback to agent:*:main, with no boilerplate or noise." + }, + "usage": { + "input_tokens": 746, + "output_tokens": 646 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English (settings, buttons, agent/skill pages, channel configs, status messages, tooltips), creating usability barriers for non-English users—notably Chinese speakers; the distributed control-ui bundle contains these hardcoded strings.", + "solution_summary": "Extract all hardcoded UI strings into an i18n layer (e.g., JSON locale files), provide a zh-CN locale with translations, add a language selector or auto-detect system language, and build infrastructure to easily add more languages.", + "maintainer_signal_summary": "This is a feature request (priority medium-high) noting docs already have zh-CN translations and the author asks whether this is on the roadmap and if contributors are welcome; implementation requires changing control-ui bundles to use locale files.", + "dedupe_summary": "Add i18n to OpenClaw Control UI by extracting hardcoded English strings into locale files and adding Simplified Chinese (zh-CN) plus a language selector/auto-detect." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: summarizes extracting hardcoded UI strings into locale files, adding Simplified Chinese (zh-CN) and a language selector/auto-detect without any template noise." + }, + "usage": { + "input_tokens": 667, + "output_tokens": 595 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "WhatsApp channel's systemPrompt support was only at the root level, so group chats could not receive per-account or per-group systemPrompt instructions to customize reply behavior.", + "solution_summary": "Extend WhatsApp config/schema to support systemPrompt at channels.whatsapp, accounts., groups. (and wildcard '*'); implement resolution hierarchy where account-level overrides root, group-specific or '*' wildcard is chosen within the active groups map, account 'groups' fully replace root 'groups', and the final injected prompt is account prompt + resolved group prompt separated by a blank line; add tests and docs.", + "maintainer_signal_summary": "Code and config schema were changed for the WhatsApp channel to add account and group-level systemPrompt resolution, with account merging and group prompt composition implemented; account groups replace root groups; no changes to other channels, hot-reload behavior, or group allowlist/mention gating; tests and documentation included.", + "dedupe_summary": "Add hierarchical WhatsApp systemPrompt resolution: support systemPrompt at root, account, and group (including '*' wildcard) levels; account-level overrides root and account 'groups' replace root 'groups'; resolved prompt is account prompt plus group prompt." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific description of the new hierarchical WhatsApp systemPrompt resolution (root/account/group with wildcard and override/merge behavior) without template noise, so it will cluster correctly with similar changes." + }, + "usage": { + "input_tokens": 2443, + "output_tokens": 714 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before entering the inbound/agent processing path, and WhatsApp group policy resolution ignored account-scoped config, causing account-level group allowlist/fallbacks to be missed and valid group messages to be rejected.", + "solution_summary": "Enable append processing only for WhatsApp group messages (keep DM append skipped) and propagate accountId/account-scoped groupAllowFrom/allowFrom into WhatsApp group policy resolution so account-scoped settings are applied correctly.", + "maintainer_signal_summary": "Scoped bugfix touching WhatsApp inbound and group-activation code (no config/schema changes, DM append still skipped, no dist/debug noise); targeted tests and traces validate the fix; risk noted that enabling group append could replay backlog messages but mitigated by scope and dedupe.", + "dedupe_summary": "WhatsApp group messages sent as messages.upsert(type=\"append\") were skipped before agent processing, and group policy resolution omitted accountId so account-scoped groupAllowFrom/allowFrom were ignored resulting in valid group messages being rejected." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies both the messages.upsert(type=\"append\") handling bug and the missing accountId in group policy resolution that caused account-scoped allowFrom settings to be ignored, which accurately captures the core issue and impact." + }, + "usage": { + "input_tokens": 1995, + "output_tokens": 587 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because client IP resolution failed: resolveClientIp returned undefined for requests from a trusted proxy that omitted forwarding headers, and isLocalDirectRequest did not fall back to the raw socket address when allowRealIpFallback was enabled. This prevented local/direct auth detection and blocked Canvas for Serve users.", + "solution_summary": "Make resolveClientIp return the socket address as a fallback when no forwarding headers are present; update isLocalDirectRequest to check req.socket.remoteAddress when allowRealIpFallback is enabled; gate .ts.net host acceptance behind an existing allowTailscale flag and thread auth.allowTailscale through callers; add tests and docs describing required trustedProxies and allowRealIpFallback settings.", + "maintainer_signal_summary": "Includes unit tests and log traces showing the regression and fix, docs updated with required config, no new config keys introduced (allowTailscale already exists and defaults to true for tailscale.mode = \"serve\"), and notes about risks and mitigations; depends on an overlapping PR for allowTailscale gating.", + "dedupe_summary": "Canvas/A2UI 401 when behind Tailscale Serve due to client-IP resolution bug: resolveClientIp returns undefined for direct trusted-proxy requests lacking XFF/X-Real-IP and isLocalDirectRequest fails to fallback to req.socket.remoteAddress with allowRealIpFallback enabled; fixed by using socket-address fallback and gating .ts.net host acceptance behind allowTailscale." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the root cause (client IP resolution and fallback failures) and the fix (socket-address fallback and .ts.net gating) that would reliably cluster with similar Tailscale/Trusted-proxy IP issues." + }, + "usage": { + "input_tokens": 1834, + "output_tokens": 765 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "checkInboundAccessControl() computed an isSelfChat heuristic but did not enforce it, allowing non-self WhatsApp messages (group messages, DMs from strangers, broadcasts) to pass through because group policy resolution defaulted to \"open\". This violates the intended self-chat isolation for accounts configured with explicit selfChatMode: true and lets unwanted messages reach downstream processing.", + "solution_summary": "Add an early-return guard in checkInboundAccessControl that rejects all non-self messages when account.selfChatMode === true, placed before any group/DM policy evaluation; the guard checks the explicit config flag (not the heuristic) and preserves self-messages via isSamePhone. No other channel access control or downstream policy logic was changed.", + "maintainer_signal_summary": "Change is small and focused to src/web/inbound/access-control.ts, tests pass (existing access-control tests), and the guard can be reverted quickly if needed. The fix intentionally does not apply to auto-detected self-chat heuristic to avoid blocking teammates; fail-closed behavior occurs if selfE164 is null.", + "dedupe_summary": "WhatsApp self-chat mode (account.selfChatMode === true) did not block non-self messages because isSelfChat was computed but unused and groupPolicy defaulted to \"open\"; fix adds an early-return in WhatsApp inbound access control to reject all non-self messages before policy evaluation." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states the bug (self-chat mode failed to block non-self messages due to an unused isSelfChat and default groupPolicy) and the fix (early-return guard in inbound access control), making it ideal for deduplication." + }, + "usage": { + "input_tokens": 1549, + "output_tokens": 652 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "With groupPolicy set to \"allowlist\", group messages from senders not in groupAllowFrom are dropped at the access-control stage and never reach the mention-gating/context-storage logic, so the agent cannot see those messages as recent context.", + "solution_summary": "Add an opt-in config flag groupContextFromAll (top-level and per-account, default false). When enabled, access-control will mark non-allowlisted group messages as storeForContext, the inbound monitor will mark them contextOnly and skip read receipts, and they will be recorded as pending group context without triggering replies.", + "maintainer_signal_summary": "Opt-in and backward-compatible (default false); changes localized to WhatsApp inbound pipeline, types, config schema, and auto-reply monitor; tested locally. Potential edge cases around allowlist/phone-number normalization and bypassing existing group gating/state updates that may cause subtle behavior differences.", + "dedupe_summary": "Introduce groupContextFromAll to preserve group messages from non-allowlisted senders as contextOnly/pending context (visible in recent chat context) instead of dropping them when groupPolicy is \"allowlist\"; context-only messages cannot trigger replies and skip read receipts." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific description of the new feature (groupContextFromAll) and its behavior (preserve non-allowlisted group messages as context-only that can't trigger replies and skip read receipts), suitable for deduplication." + }, + "usage": { + "input_tokens": 1566, + "output_tokens": 747 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "sessions_spawn always created a new random-UUID sub-agent session, preventing deterministic reuse of a sub-agent's conversation history and risking cross-agent session injection if an external session identifier were allowed unchecked.", + "solution_summary": "Add an optional sessionKey parameter to sessions_spawn that, when provided, maps short keys to a namespaced session id (agent:{agentId}:subagent:{key}), accepts fully-qualified session ids only if their embedded agentId matches the target agent, and falls back to crypto.randomUUID() when omitted; includes tests for deterministic keys, UUID fallback, fully-qualified passthrough, and mismatch rejection.", + "maintainer_signal_summary": "Tests currently contain a broken import (openclaw-tools.js vs clawdbot-tools.js) that prevents compilation; cross-agent validation has a case-sensitivity bug where embedded agentId is not normalized causing false mismatches; error handling is inconsistent (throws vs returning a forbidden jsonResult), which may alter caller behavior.", + "dedupe_summary": "Add optional sessionKey to sessions_spawn to enable deterministic sub-agent sessions (short keys namespaced to agent:{agentId}:subagent:{key}, fully-qualified keys validated to match target agentId), with fallback to random UUID." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change (optional sessionKey, namespacing rules, validation, and UUID fallback) without boilerplate, so it will match similar PRs/issues." + }, + "usage": { + "input_tokens": 1619, + "output_tokens": 556 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) fails because the Telegram adapter calls editMessageText, causing a 400 error 'there is no text in the message to edit' and leaving message(action=edit) requests failing for captionable messages. Affected component: src/telegram/send.ts (editMessageTelegram).", + "solution_summary": "editMessageTelegram now catches the specific 400 error (NO_TEXT_IN_MESSAGE_RE) and retries using editMessageCaption for media messages; buttons-only updates use editMessageReplyMarkup directly; MESSAGE_NOT_MODIFIED is treated as success and HTML parse fallback preserved. Six unit tests were added to cover retry, buttons-only, error propagation, and MESSAGE_NOT_MODIFIED handling.", + "maintainer_signal_summary": "Change is backward-compatible with no interface or config changes and only affects the Telegram channel adapter; six new unit tests added; risk that the regex may not match if Telegram changes the error wording (unmatched errors will continue to propagate as before). Revert by restoring src/telegram/send.ts.", + "dedupe_summary": "Telegram edit fails for media messages because adapter calls editMessageText (400: 'there is no text in the message to edit'); fix by detecting that error and retrying with editMessageCaption (use editMessageReplyMarkup for buttons-only)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary that clearly states the Telegram API error when editing media captions and the adapter change to detect the 400 and retry with editMessageCaption (and use editMessageReplyMarkup for buttons-only), so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 1500, + "output_tokens": 736 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "WhatsApp integration was hardcoded to disable block streaming (disableBlockStreaming: true), causing all messages to be batched and sent only after the agent finished even when per-account or global block streaming settings requested progressive delivery. This broke expected streaming behavior for users who enabled block streaming.", + "solution_summary": "Restore config-driven block streaming by reading per-account channels.whatsapp.blockStreaming and falling back to agents.defaults.blockStreamingDefault; adjust the deliver callback to only suppress tool payloads (reasoning payloads remain filtered upstream by shouldSuppressReasoningPayload).", + "maintainer_signal_summary": "Change touches extensions/whatsapp message delivery logic to respect per-account/global blockStreaming settings and limits suppression in the deliver callback to tool payloads; reasoning suppression is unchanged and upstream filtering still prevents leaking thinking content. Tests and live verification show progressive delivery restored and no reasoning/tool leaks; default behavior remains off unless enabled.", + "dedupe_summary": "WhatsApp integration ignores blockStreaming config and batches messages because disableBlockStreaming was hardcoded; needs to be made config-driven so per-account/global blockStreaming settings enable progressive block streaming." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states that WhatsApp ignored blockStreaming due to a hardcoded disableBlockStreaming and must be made config-driven to respect per-account/global settings and enable progressive streaming." + }, + "usage": { + "input_tokens": 1492, + "output_tokens": 407 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Sending video/media via the Telegram channel could cause the gateway to hang and become completely unresponsive. Root cause: content-length checks triggered before consuming the HTTP response body, leaving an unconsumed body that causes undici Agent.close()/closeDispatcher to deadlock.", + "solution_summary": "Increase default channels.telegram.mediaMaxMb from 5 to 20; fix closeDispatcher to impose a timeout and fallback to destroy the agent to avoid deadlock; make media download failures non-fatal (still forward text/captions); add timeouts and retry logic to getFile and download operations; strip inbound media path metadata from UI.", + "maintainer_signal_summary": "Changes affect gateway/orchestration and Telegram integration (not SSRF policy): key files src/infra/net/ssrf.ts and src/telegram/bot.ts; backward compatible and configurable via channels.telegram.mediaMaxMb; tests added and verified on live bot; risk: 3s closeDispatcher timeout may prematurely destroy connections but is reversible and default limit can be lowered to 5 for rollback.", + "dedupe_summary": "Gateway hangs when receiving large Telegram media because content-length rejection leaves the response body unconsumed, causing undici Agent.close()/closeDispatcher to block; fix implemented by raising mediaMaxMb, adding timeout+destroy fallback for dispatcher close, and making media download errors non-fatal with retries." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific description of the root cause (unconsumed response body causing undici Agent/dispatcher deadlock) and the implemented fixes (increase media limit, timeout/destroy fallback, non-fatal downloads with retries), so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 1588, + "output_tokens": 870 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "Telegram integration fallback body text used a hardcoded '' placeholder for non-audio media when no caption/text was present, causing document (PDF/DOCX) and other non-photo attachments to be misidentified to agents.", + "solution_summary": "Change resolveTelegramInboundBody to use the already-computed, type-aware placeholder variable so the fallback emits '', '', etc., and add unit tests verifying placeholders and media handling for document messages.", + "maintainer_signal_summary": "Small, scoped bugfix in extensions/telegram: only the agent-facing placeholder text was changed; media download and extraction logic unchanged. New unit tests were added; watch for downstream code that pattern-matches on '' in this narrow fallback case.", + "dedupe_summary": "Telegram inbound body fallback incorrectly emitted '' for document and other non-photo media when no caption/text was present; needs to use the type-aware ''/'' placeholder instead." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical, and focused on the core bug and intended fix (use type-aware media placeholder instead of hardcoded ), with no boilerplate or extraneous details." + }, + "usage": { + "input_tokens": 1415, + "output_tokens": 430 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "MEDIA_MAX_BYTES was hardcoded (5 MB) in src/media/store.ts so users cannot change the inbound media staging limit; files larger than 5 MB silently fail to stage into sandboxed agents, preventing processing of common large files like PDFs and high-res images.", + "solution_summary": "Introduce a new optional config field media.maxBytes (integer, default 5 MB, max 500 MB) and replace the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes() usage; the gateway reads media.maxBytes at boot, error messages now report the configured limit, and default behavior remains 5 MB when unset.", + "maintainer_signal_summary": "Change preserves backward compatibility (default 5 MB and MEDIA_MAX_BYTES exported as deprecated), schema enforces a 500 MB cap, tests pass (56 unit tests), bot review threads were addressed, and the change can be reverted by removing media.maxBytes from config.", + "dedupe_summary": "Hardcoded 5MB media staging limit prevents staging files >5MB into sandboxed agents; add a configurable media.maxBytes (default 5MB, cap 500MB) and replace the constant with runtime-configurable getters/setters." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core problem (hardcoded 5MB staging limit) and the precise change (add media.maxBytes config and replace constant with runtime getters/setters), so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 1536, + "output_tokens": 631 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "Telegram integration was silently dropping messages when media download failed (due to exceeding mediaMaxMb or Telegram Bot API limits) because the handler returned early; as a result the agent never received the message or ack and users got no indication their file/caption was lost. The affected component is the Telegram bot message handling (bot-handlers.ts), causing complete message loss including captions and text when downloads failed.", + "solution_summary": "On download failure, extract file metadata (name, size, type) from the Telegram message object, build a metadata stub and inject it into msg.text, and allow the message to continue through the normal pipeline instead of returning early; this lets the agent see warnings like \"⚠️ [File: ...] (exceeds 5MB download limit)\" and respond accordingly.", + "maintainer_signal_summary": "Bugfix in src/telegram/bot-handlers.ts for the Telegram integration; touches error paths for media downloads only (no change to normal map to SignalAccountConfig and Zod schema; wire resolveChannelGroupPolicy into the Signal event handler and commandGate authorizers so listed groups bypass sender-level checks and are authorized for control commands; update docs.", + "maintainer_signal_summary": "Implementation follows existing patterns from other channels, addresses Greptile review feedback (ensuring allowed groups can run commands), passes TypeScript checks, supersedes PR #4337, and fixes issue #25540; safe to merge with low risk.", + "dedupe_summary": "Signal channel: add channels.signal.groups. allowlist so listed groups bypass sender-level groupAllowFrom and are authorized for messages and control commands." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that names the specific config key and clearly states the functional change (group-level allowlist bypassing sender-level checks for messages and commands)." + }, + "usage": { + "input_tokens": 1130, + "output_tokens": 641 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked group-level allowlist support: when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter and never matches a group ID.", + "solution_summary": "Added a groups config to SignalAccountSchemaBase and invoked resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom; added four tests to cover group-level policy resolution.", + "maintainer_signal_summary": "Small Signal-specific fix (schema + event-handler change) with 4 new tests; all config tests pass and it resolves silent-dropping of group messages under groupPolicy=\"allowlist\" (fixes the reported issue).", + "dedupe_summary": "Signal ignores group-level allowlists: group IDs placed in groupAllowFrom are treated as sender identifiers so group messages are dropped; missing channels.signal.groups config and resolveChannelGroupPolicy call caused the bug." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: explains that Signal treated group IDs as sender IDs causing dropped group messages and notes the missing channels.signal.groups config/resolveChannelGroupPolicy, which is exactly the core bug for deduplication." + }, + "usage": { + "input_tokens": 930, + "output_tokens": 657 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "WhatsApp channel always coalesces agent output into a single final message because process-message.ts hardcodes disableBlockStreaming: true, so intermediate blocks (tool outputs/sleeps) are not delivered as separate messages.", + "solution_summary": "Make WhatsApp respect the channels.whatsapp.blockStreaming config by adding resolveWhatsAppBlockStreamingEnabled(), replacing the hardcoded flag with the config-driven value, treating intermediate blocks as valid replies when blockStreaming is enabled, registering the capability and default in the channel dock, and updating tests.", + "maintainer_signal_summary": "Change is small and backwards-compatible (default behavior remains coalescing when blockStreaming is false/omitted), tests updated to cover enabled/disabled behavior, registers new capability and default constant in dock, and fixes queuedFinal logic to match Telegram/Line behavior; labeled channel: whatsapp-web, app: web-ui, agents, size: S.", + "dedupe_summary": "WhatsApp channel hardcodes disableBlockStreaming:true causing all message blocks to be batched; should instead respect channels.whatsapp.blockStreaming so intermediate blocks can be delivered as separate WhatsApp messages when enabled." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the bug and intended config-driven change without boilerplate or extraneous details, so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 680, + "output_tokens": 456 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v3-examples.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v3-examples.json new file mode 100644 index 0000000..d9d7474 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v3-examples.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v3-examples", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "Summarize this GitHub issue or pull request for duplicate detection clustering. The dedupe_summary will be embedded as a vector — it must be clean signal with zero noise.\n\nREMOVE all template noise. Examples of what to strip:\n- \"### Bug type\\nRegression (worked before, now fails)\" → ignore, it's a template field\n- \"### Steps to reproduce\\n1. Go to settings\\n2. Click...\" → ignore reproduction steps\n- \"### Expected behavior\\nIt should work\" → ignore\n- \"- [x] Tests added\\n- [x] Docs updated\" → ignore checklist\n- \"### Environment\\nOS: macOS 14.2, Node 20.1\" → ignore unless the OS IS the bug\n- \"Fixes #1234\" → ignore cross-references\n- \"## Testing\\nRun `pnpm test`...\" → ignore testing instructions\n\nKEEP the core signal:\n- \"Discord REST API ignores proxy configuration\" → THIS is the signal\n- \"WhatsApp group messages not delivered when streaming enabled\" → THIS is the signal\n- \"Telegram media download hangs on large video files\" → THIS is the signal\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nPlain text, no markdown, 1-3 sentences each.\ndedupe_summary must be a specific, searchable description of the core issue or change.", + "timestamp": "2026-03-30T16:46:19.491Z", + "aggregate": { + "avg_score": 4.95, + "avg_signal_density": 4.95, + "boilerplate_count": 0, + "boilerplate_pct": 0, + "would_cluster_correctly_pct": 100, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 49863, + "total_output_tokens": 27548 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "channels.discord.proxy only applies to the Discord WebSocket gateway; REST API calls at startup (deploy commands, fetch bot identity) and guild resolution bypass the configured HTTP proxy and time out, leaving guild channels unresolved while DMs work.", + "solution_summary": "Apply the configured proxy to the undici HTTP dispatcher used for all Discord REST calls and guild resolve paths (not just the WebSocket), or add a global proxy config that sets a ProxyAgent; add retries for guild resolution.", + "maintainer_signal_summary": "Reproducible behind an HTTP proxy: WebSocket connects via proxy and DMs work but startup REST calls fail with \"fetch failed\" and logs show channels unresolved:guild:. setGlobalDispatcher(ProxyAgent) fixes REST calls but guild resolve still uses an unproxied path and remains unresolved.", + "dedupe_summary": "channels.discord.proxy only proxies the Gateway WS; REST API startup calls (deploy-rest, fetch-bot-identity) and guild resolve bypass proxy causing fetch failed timeouts; undici/Node fetch ignores HTTP_PROXY and a global ProxyAgent is needed; guild resolve uses a separate unproxied code path." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that identifies the core regression (only Gateway WS proxied while REST startup calls and guild-resolve bypass the proxy) and the likely root cause (undici/Node fetch ignoring HTTP_PROXY requiring a global ProxyAgent), so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 2382, + "output_tokens": 554 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "Discord bot receives messages over the gateway via proxy but cannot send messages because outbound REST requests fail with \"fetch failed\". channels.discord.proxy is logged as enabled but Carbon's RequestClient.fetch does not use the proxy, so REST API calls bypass the proxy and fail in restricted networks.", + "solution_summary": "Update RequestClient.fetch to honor channels.discord.proxy by routing REST requests through the configured proxy (support HTTP/SOCKS). Apply the proxy consistently to all Discord REST endpoints including attachment uploads/downloads, add tests and config validation, and integrate a proxy agent or env-based proxy support.", + "maintainer_signal_summary": "Reproduced on OpenClaw 2026.2.25/2026.2.26 (macOS) in a China environment requiring a proxy; logs show REST proxy enabled yet REST sends fail with TypeError: fetch failed. Root cause: Carbon RequestClient.fetch lacks proxy handling and so bypasses configured proxy for REST/attachment requests.", + "dedupe_summary": "channels.discord.proxy not applied to REST API sends because Carbon RequestClient.fetch lacks proxy support, causing fetch failed for outbound Discord REST requests while WebSocket gateway works." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies channels.discord.proxy not being applied to REST sends due to Carbon RequestClient.fetch lacking proxy support, and contrasts REST failure with working WebSocket, making it ideal for clustering similar reports." + }, + "usage": { + "input_tokens": 1064, + "output_tokens": 760 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "When OpenClaw is configured behind an HTTP proxy, the Discord provider can log in and the gateway works but some startup REST calls (native command deployment and bot identity fetch) consistently fail with 'fetch failed', leaving the provider half-functional.", + "solution_summary": "Unify proxy handling so all Discord REST requests use the same proxy-aware fetch/HTTP dispatcher as the gateway/login path; propagate the configured proxy to the REST client used for deploy commands and bot identity fetch (or use a shared HTTP agent).", + "maintainer_signal_summary": "Logs show rest proxy enabled and applicationId fetch succeeds, gateway proxy enabled and Discord logs in, but deploy-rest PUT to /applications/:id/commands and fetch-bot-identity both error with 'fetch failed'; issue repeats on health-monitor restarts. Suspected cause: some REST request paths are not using the configured proxy or the same HTTP transport as the gateway/WebSocket path.", + "dedupe_summary": "Discord provider logs in via proxy but startup REST calls (deploy native commands and fetch bot identity) fail with 'fetch failed' due to inconsistent/missing proxy handling for some REST request paths." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that states the core problem (gateway login succeeds via HTTP proxy while specific startup REST calls — native command deployment and bot identity fetch — fail due to inconsistent/missing proxy handling), so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 1210, + "output_tokens": 767 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "OpenClaw logs show the Discord proxy as enabled but REST API calls fail with 'fetch failed' and the gateway WebSocket never connects (gatewayConnected=false) when running under WSL2 with an HTTP proxy configured.", + "solution_summary": "Fix the Discord REST client to actually use the configured HTTP proxy (apply proxy agent or ensure node fetch used by Discord.js respects channels.discord.proxy) so REST calls succeed and the bot can authenticate and establish the gateway WebSocket.", + "maintainer_signal_summary": "OpenClaw 2026.3.13 on WSL2 (Windows 11) with channels.discord.proxy=\"http://127.0.0.1:10808\"; curl via the proxy to Discord API returns 200, OpenClaw logs 'discord: rest proxy enabled' and 'discord: gateway proxy enabled' but REST calls fail ('deploy-rest:put:error ... error=fetch failed', 'failed to fetch bot identity: TypeError: fetch failed') and gatewayConnected stays false; reproducible when a proxy is required; Feishu channel works in same environment.", + "dedupe_summary": "Discord proxy setting is logged but Discord.js REST/fetch ignores the proxy, causing 'fetch failed' errors and failed bot authentication under WSL2 with channels.discord.proxy set." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states that the configured Discord proxy is logged but ignored by Discord.js REST/fetch, causing 'fetch failed' errors and failed bot authentication, which will cluster correctly with similar proxy/REST issues." + }, + "usage": { + "input_tokens": 1047, + "output_tokens": 698 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "OpenClaw's Discord REST API calls ignore the configured proxy and fail with \"TypeError: fetch failed\" while the Discord WebSocket connection and other channels (e.g., Telegram) work through the proxy; curl to the Discord REST endpoint via the same proxy succeeds.", + "solution_summary": "Route Discord REST API HTTP calls through the same proxy-aware fetch/SSRF guard path used for web_search/web_fetch so they honor channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY and avoid the fetch failures.", + "maintainer_signal_summary": "OpenClaw 2026.2.26 on macOS; logs show \"discord final reply failed: TypeError: fetch failed\". channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY/ALL_PROXY set but REST calls still fail; curl via proxy to Discord API succeeds; WebSocket and Telegram REST use the proxy correctly. Changelog note: web tools were fixed in 2026.2.26 but Discord REST was not included.", + "dedupe_summary": "Discord REST API client in OpenClaw ignores configured proxy/HTTP(S)_PROXY and fails with \"TypeError: fetch failed\", while WebSocket and other channels work and curl via the proxy succeeds." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "The summary concisely and specifically states that OpenClaw's Discord REST client ignores proxy settings (including HTTP(S)_PROXY) and fails with 'TypeError: fetch failed', contrasting with working WebSocket and curl via the proxy, which is the core signal needed for deduplication." + }, + "usage": { + "input_tokens": 1070, + "output_tokens": 740 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "The CLI command openclaw agent --channel discord --to delivers messages to agent:*:main instead of the live Discord channel/thread session because the session resolver never searched by the real delivery context (channel + to + accountId), command path didn’t propagate channel/account info, and Discord targets were not normalized.", + "solution_summary": "Manual patches were applied to compiled dist files to add lookup by delivery context, normalize Discord targets (e.g., 148... → channel:148...), pass channel and replyAccount/accountId through the command path, and fix the compact resolver to use buildAgentMainSessionKey; after the patch the CLI routes to the correct live session.", + "maintainer_signal_summary": "Affected compiled files: dist/gateway-cli-BjsM6fWb.js, dist/compact-1mmJ_KWL.js, dist/register.agent-DHoJWl4M.js. Changes introduce high regression risk because they modify dist files directly; recommend implementing the fix in source, adding normalization and delivery-context lookup in resolver logic, and revalidating after updates.", + "dedupe_summary": "openclaw CLI 'agent --channel discord --to' incorrectly routes to agent:*:main because the session resolver lacks delivery-context lookup (channel+to+accountId) and does not normalize Discord targets; patched dist files add delivery-context lookup and target normalization." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (CLI routing wrong session) and root causes (missing delivery-context lookup and Discord target normalization) without extraneous template text." + }, + "usage": { + "input_tokens": 997, + "output_tokens": 760 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions in Discord handling: volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) was re-injected every turn causing progressive context degradation in threads; CLI agent commands resolved to agent:*:main instead of the live thread session, delivering to the wrong session.", + "solution_summary": "Prevented injection of volatile Discord thread metadata and updated session resolution to use actual delivery context by normalizing Discord targets and passing channel plus replyAccount/accountId to the resolver; replaced incorrect resolveAgentMainSessionKey usage with buildAgentMainSessionKey(...). Changes applied to compiled runtime bundles in dist (pi-embedded, gateway-cli, compact, register.agent).", + "maintainer_signal_summary": "Fixes were applied directly to compiled bundles in openclaw/dist and require re-application after updates; openclaw-gateway.service was restarted to verify writer/reasoning sessions show proper toolCall output and Discord threads no longer load repeated context. CLI now routes by actual Discord target instead of defaulting to main.", + "dedupe_summary": "Discord threads: repeated injection of volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) every turn causing context bloat; CLI resolver defaulted to agent:*:main instead of resolving by channel+to+accountId — fixed by blocking metadata re-injection and normalizing delivery context in the session resolver." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "The summary concisely captures the two technical regressions (volatile metadata re-injection causing context bloat and CLI session resolver falling back to agent:*:main instead of using channel+to+accountId) without boilerplate, so it will correctly cluster similar issues." + }, + "usage": { + "input_tokens": 916, + "output_tokens": 659 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "Sandbox media staging cap is hardcoded to 5 MB (const MEDIA_MAX_BYTES = 5 * 1024 * 1024 in dist/store-*.js), so files larger than 5 MB are rejected or silently fail when copied into sandboxed agents.", + "solution_summary": "Expose MEDIA_MAX_BYTES as a configurable value read from openclaw.json (for example tools.media.maxBytes) with a 5 MB default, and update store code to use that config before per-type checks.", + "maintainer_signal_summary": "Constant MEDIA_MAX_BYTES is defined in dist/store-*.js and has been hardcoded since at least v2026.3.7 (still present in v2026.3.8). Reproduce by running sandbox.mode: \"non-main\" and sending a file >5 MB. Temporary workaround: sed-patch dist/store-*.js after npm install. Suggested config key: tools.media.maxBytes; per-type keys tools.media.image.maxBytes and tools.media.audio.maxBytes already exist.", + "dedupe_summary": "Sandbox media staging limit MEDIA_MAX_BYTES hardcoded to 5 MB in dist/store-*.js and not configurable via openclaw.json, causing files >5MB to be rejected." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core issue (hardcoded MEDIA_MAX_BYTES in dist/store-*.js, not configurable via openclaw.json, causing >5MB files to be rejected) with no extraneous boilerplate." + }, + "usage": { + "input_tokens": 963, + "output_tokens": 813 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "Discord bot receives messages via Gateway but all Discord REST API requests fail with \"fetch failed\" even though proxy is configured and logs show \"rest proxy enabled\"; curl using the same proxy succeeds and Telegram works with the same proxy.", + "solution_summary": "Fix the Discord REST client to honor the configured HTTP proxy (pass a proxy-aware agent or use a proxy-capable fetch), ensure the REST proxy code path isn't being skipped, and add tests/logging to validate REST requests use the proxy.", + "maintainer_signal_summary": "Gateway logs indicate \"rest proxy enabled\" and WebSocket (Gateway) connections work; REST operations (deploy commands, fetch bot identity, sending messages) return TypeError: fetch failed; direct curl requests through the same proxy succeed, isolating the failure to the app's Discord REST proxy path.", + "dedupe_summary": "Discord REST API requests fail with \"fetch failed\" despite rest proxy enabled and the same proxy working via curl" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core issue—REST API calls erroring with 'fetch failed' despite proxy being enabled and usable via curl—no boilerplate or noisy details." + }, + "usage": { + "input_tokens": 908, + "output_tokens": 711 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "Discord REST API calls made via @buape/carbon's RequestClient (and related fetch uses like memorySearch and attachment downloads) ignore the channels.discord.proxy setting in openclaw.json; only the WebSocket gateway respects the proxy. RequestClient calls globalThis.fetch without a proxy-aware dispatcher, and Node.js 22's native fetch does not read HTTP(S)_PROXY env vars by default.", + "solution_summary": "Change @buape/carbon's RequestClient to accept an optional dispatcher/proxy configuration and have OpenClaw pass channels.discord.proxy to it so REST API calls use the same proxy as the WebSocket. As a temporary workaround, preload an undici EnvHttpProxyAgent via NODE_OPTIONS to set a global dispatcher, but avoid using --use-env-proxy because it breaks WebSocket connections.", + "maintainer_signal_summary": "RequestClient currently invokes globalThis.fetch with no proxy dispatcher, causing REST requests, memorySearch, and some media downloads to bypass configured proxy; fix by exposing/accepting a dispatcher or proxy option and forwarding openclaw.json channels.discord.proxy to it. Do not recommend NODE_OPTIONS=--use-env-proxy because it interferes with WebSocket upgrades.", + "dedupe_summary": "@buape/carbon RequestClient uses globalThis.fetch without a proxy-aware dispatcher, so Discord REST requests ignore openclaw.json channels.discord.proxy (Node.js fetch doesn’t honor env proxy by default); add dispatcher/proxy support to RequestClient." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and technical: it states that @buape/carbon's RequestClient uses globalThis.fetch (which doesn't respect proxy env) and causes Discord REST calls to ignore channels.discord.proxy, recommending dispatcher/proxy support." + }, + "usage": { + "input_tokens": 917, + "output_tokens": 686 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "OpenClaw shows performance degradation under sustained load with long-running conversations: unbounded context token growth (writer ~42k tokens, reasoning ~90k), Discord lane blocking causing serialization under load, and typing indicators stuck until TTL in some code paths. High concurrency settings and insufficient event queue timeouts exacerbate the issue.", + "solution_summary": "Reduced concurrency (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents maxConcurrent 8→4, maxChildrenPerAgent 8→4), set agents.defaults.contextTokens to 80000, and increased channels.discord.eventQueue.listenerTimeout from 120000 to 300000; improved patching script to cover all hash variants. Gateway restarted and errors (lane wait exceeded, typing TTL) ceased, though restarts are not a complete fix under sustained load.", + "maintainer_signal_summary": "Key observables: session token counts grew to ~42k (writer) and ~90k (reasoning), lane blocking and serialization by agentId under load, and typing indicators stuck until TTL. Applied config changes and restart reduced errors; recommend making context/concurrency limits adaptive or exposing them in openclaw config.", + "dedupe_summary": "Unbounded conversation context token growth plus high concurrency causes lane blocking/serialization and sticky typing indicators; mitigated by lowering maxConcurrentSessions/agent/subagent limits, setting agents.defaults.contextTokens=80000, and increasing Discord eventQueue listenerTimeout to 300000." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the root causes (unbounded context growth and high concurrency causing lane blocking/typing issues) and the applied mitigations, making it well-suited for clustering similar reports." + }, + "usage": { + "input_tokens": 851, + "output_tokens": 593 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11, Discord threads re-inject volatile metadata on every conversation turn (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply), causing context contamination, token growth, degraded performance, and prompt cache invalidation.", + "solution_summary": "Implement the fix in source: limit injection of thread-starter and volatile metadata to the first turn via an isNewSession guard, replace resolveAgentMainSessionKey with buildAgentMainSessionKey where needed, add tests and post-update validation, and rebuild so compiled dist no longer re-injects metadata.", + "maintainer_signal_summary": "Affected compiled files: dist/pi-embedded-D6PpOsxP.js (metadata injection) and dist/compact-1mmJ_KWL.js (missing isNewSession guard and wrong session-key call). Apply fix in source to stop per-turn injection of message_id/reply_to_id/timestamp/Thread starter/Chat history, add isNewSession guard, switch to buildAgentMainSessionKey(...), add tests and a post-update revalidation script; current manual dist patch is high-risk and will be overwritten on updates.", + "dedupe_summary": "Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution and prompt-cache bust; compiled files dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js inject metadata and lack an isNewSession guard." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that states the core bug (Discord threads re-inject volatile metadata every turn), lists the exact fields affected and implicated compiled files, and notes the missing session guard—suitable for clustering similar reports." + }, + "usage": { + "input_tokens": 863, + "output_tokens": 882 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, repeated prompt content, degraded performance on long threads, and prompt-cache busting that increases token costs.", + "solution_summary": "A manual patch was applied to the compiled artifact dist/pi-embedded-D6PpOsxP.js to stop re-injecting the listed metadata after the first turn; validation shows repeated context stopped, prompt caching restored, and token usage reduced. Recommend implementing the fix in source and adding a post-update revalidation to avoid losing the change on upgrades.", + "maintainer_signal_summary": "Affected file: dist/pi-embedded-D6PpOsxP.js; root cause is a missing guard that limits Discord thread metadata injection to the first turn (unlike Slack/iMessage). High regression risk because the fix is applied to compiled files—priority: commit source-level fix, add tests, and ensure upgrade-safe deployment or revalidation script.", + "dedupe_summary": "Discord threads re-inject message_id, reply_to_id, timestamp, Thread starter, and Chat history on every turn in v2026.3.11; patched in dist/pi-embedded-D6PpOsxP.js to inject this metadata only on the first turn." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary clearly states the core bug and lists the exact metadata re-injected (good for clustering), but includes version and patch/file details that are unnecessary for deduplication." + }, + "usage": { + "input_tokens": 871, + "output_tokens": 993 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "Two regressions in Discord thread handling: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing progressive context growth and a session resolver fallback to agent:*:main that routes CLI/session commands to the wrong session, leading to increased latency and degraded performance in long threads.", + "solution_summary": "Applied runtime patches to compiled dist bundles (dist/reply-BCcP6j4h.js, dist/compact-1mmJ_KWL.js, dist/pi-embedded-Cz5VjpnY.js, dist/gateway-cli-B-E8XzUM.js, dist/register.agent-jS8B5meo.js), restarted the gateway, created backups (*.bak-rootfix-20260313T023340Z) and an idempotent reapply script (/home/clawd/scripts/openclaw-rootfix-v2026.3.11.sh), and validated thread-bound session resolution and logs.", + "maintainer_signal_summary": "Manually patched compiled runtime bundles listed above; patches are not preserved across reinstalls so maintain the reapply script and backups. Gateway restarted (active) and logs show no new errors. Recommend adding smoke tests for long-thread context size growth and CLI routing correctness.", + "dedupe_summary": "Discord threads: per-turn metadata re-injection causing unbounded context growth and session resolver fallback to agent:*:main (wrong CLI/session routing); patched compiled dist bundles, added backups and a reapply script." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary accurately states the core bug (per-turn metadata re-injection causing unbounded context growth and session resolver fallback to agent:*:main) and the code area patched, though mentioning backups/reapply script adds minor operational noise not needed for dedupe." + }, + "usage": { + "input_tokens": 847, + "output_tokens": 875 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "OpenClaw receives Discord messages and generates responses, but sending the reply fails with \"TypeError: fetch failed\" even though Discord rest/gateway proxy is configured and logged as enabled and the same proxy works with curl.", + "solution_summary": "Ensure the Discord reply send path applies the configured HTTP proxy/agent to fetch calls (or reads proxy env) and add logging/tests to verify the fetch request uses the proxy; likely fix is to attach the proxy agent to REST requests in the reply code path.", + "maintainer_signal_summary": "Regression in OpenClaw 2026.3.2: bot logs show rest/gateway proxy enabled and successful login, messages are received and model responses generated, but reply sending errors with \"TypeError: fetch failed\"; curl through the proxy succeeds, indicating the reply REST requests are not using the configured proxy.", + "dedupe_summary": "Discord reply sending fails with \"TypeError: fetch failed\" despite configured and logged proxy; receiving messages works and curl via proxy succeeds, indicating the reply REST/fetch path isn't using the HTTP proxy." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific description of the core bug (Discord replies failing with fetch/proxy issue and REST path likely bypassing proxy) with no template noise, so it will cluster with similar reports." + }, + "usage": { + "input_tokens": 787, + "output_tokens": 813 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "openclaw v2026.3.11 CLI 'openclaw agent --channel discord --to ...' resolves and delivers to agent:*:main instead of the live session for the specified Discord channel/thread because the session resolver does not lookup by delivery context and Discord targets are not normalized.", + "solution_summary": "Manually patched dist/gateway-cli-BjsM6fWb.js to normalize Discord targets ('148...' → 'channel:148...') and to resolve sessions by delivery context (channel+to+accountId) before falling back to agent:*:main; validated routing to correct session. Note: change is in compiled dist/ and must be ported to source to avoid loss on reinstall.", + "maintainer_signal_summary": "Implement delivery-context-based session lookup and Discord target normalization in source code (not just dist), add CLI routing tests, and replace the ad-hoc compiled-file patch to prevent regression on updates.", + "dedupe_summary": "CLI 'openclaw agent --channel discord --to ...' routes to agent:*:main because session resolver omits lookup by delivery context (channel+to+accountId) and fails to normalize Discord targets ('148...' vs 'channel:148...')." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it highlights the CLI routing to agent:*:main caused by the session resolver not looking up delivery context and failing to normalize Discord targets, which is exactly the core issue for clustering similar session-resolution/routing bugs." + }, + "usage": { + "input_tokens": 852, + "output_tokens": 819 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 a regression in the compiled file dist/compact-1mmJ_KWL.js calls an undefined function resolveAgentMainSessionKey, causing a ReferenceError, agent command failures, and potential gateway crashes.", + "solution_summary": "Manually patched dist/compact-1mmJ_KWL.js by replacing resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...); after the change the ReferenceError is gone and agent/session behavior is restored.", + "maintainer_signal_summary": "This is a high-risk manual edit to compiled dist artifacts and will be lost on update; the correct fix must be applied to the source/build pipeline so the session-resolver patch and this replacement are both present in future builds.", + "dedupe_summary": "ReferenceError: resolveAgentMainSessionKey is not defined in dist/compact-1mmJ_KWL.js — replace with buildAgentMainSessionKey(...) to fix" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the runtime ReferenceError in the compiled file and the exact code change needed, with no extraneous boilerplate, so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 823, + "output_tokens": 640 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "Discord channel plugin shows ON | OK but health check and message send fail with 'fetch failed' when OpenClaw is behind a proxy; channels.discord.proxy is applied to WebSocket but not to REST API calls, while curl with the same proxy succeeds.", + "solution_summary": "Ensure the Discord plugin applies channels.discord.proxy to its HTTP/REST client (health check and message send) in addition to WebSocket connections; update client initialization, add tests, and verify health check uses the proxy.", + "maintainer_signal_summary": "Regression on OpenClaw 2026.3.8: configuring channels.discord.proxy does not route REST API requests through the proxy, causing health check 'fetch failed' and message send timeouts; curl with the proxy works, reproduces by configuring channels.discord.proxy and running openclaw message send.", + "dedupe_summary": "Discord plugin ignores channels.discord.proxy for REST API calls (health check and message send) — only WebSocket uses the proxy, causing 'fetch failed' in proxied environments." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (proxy not applied to REST API calls causing health check and sends to fail) without boilerplate or extraneous details." + }, + "usage": { + "input_tokens": 779, + "output_tokens": 625 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the session resolver in dist/compact-1mmJ_KWL.js for the agent command does not lookup by delivery context (channel + to + accountId), so sessions resolve to agent:*:main instead of the live session for the target channel, causing agent responses to be associated with the wrong session and routing inconsistencies with the CLI gateway.", + "solution_summary": "Manually patched dist/compact-1mmJ_KWL.js to add delivery-context lookup in the agent command session resolver (search by channel + to + accountId) and only fallback to main when no specific session is found; validation shows correct session resolution and response association.", + "maintainer_signal_summary": "High regression risk: this is a manual edit to a compiled file in dist and will be lost on reinstall/update; the fix must be applied in source and rebuilt to be permanent and avoid future regressions.", + "dedupe_summary": "dist/compact-1mmJ_KWL.js session resolver missing delivery-context lookup (channel+to+accountId), causing default to agent:*:main; manual patch added delivery-context lookup but change exists only in compiled dist file." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the affected compiled file, the missing delivery-context lookup (channel+to+accountId), the failure mode (falls back to agent:*:main), and that a manual patch exists only in the dist file." + }, + "usage": { + "input_tokens": 793, + "output_tokens": 603 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English, creating usability barriers for non-English users, particularly Chinese speakers, despite the docs already having zh-CN translations.", + "solution_summary": "Extract all hardcoded UI strings into a structured i18n layer (e.g., JSON locale files), add a zh-CN locale file with translations, provide a language selector or auto-detect, and build infrastructure to add more languages.", + "maintainer_signal_summary": "Strings are currently hardcoded in the control-ui bundle (dist/control-ui/assets/index-*.js); docs/zh-CN exists and can be reused for translations. Recommended scope: extract strings, add zh-CN, add UI language selector/auto-detect, and update build to load locales.", + "dedupe_summary": "Extract hardcoded English strings from OpenClaw Control UI into locale files and add zh-CN translations plus a language selector/auto-detect." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states extracting hardcoded strings to locale files, adding zh-CN translations, and introducing language selection/auto-detect, which accurately captures the i18n feature request for the OpenClaw Control UI." + }, + "usage": { + "input_tokens": 714, + "output_tokens": 475 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "WhatsApp group chats could not apply channel-configured systemPrompt instructions at account or group granularity, preventing per-account or per-group reply customization and wildcard defaults.", + "solution_summary": "Added systemPrompt support to WhatsApp config/schema at channels.whatsapp, accounts., and groups. (including wildcard \"*\"); implemented account resolution, group prompt composition, tests and docs; account-level values override root and account.groups fully replace root groups; final injected prompt is account prompt plus resolved group prompt separated by a blank line.", + "maintainer_signal_summary": "Change scope limited to WhatsApp: no changes to non-WhatsApp channels, hot-reload behavior, or group allowlist/mention-gating semantics; includes clear resolution hierarchy (account prompt > group prompt, specific group > wildcard, account.groups replaces root groups).", + "dedupe_summary": "WhatsApp: add systemPrompt hierarchy — support systemPrompt at channels.whatsapp, accounts., and groups. (with \"*\" wildcard); account-level overrides root and account.groups replace root groups; final prompt = account prompt + resolved group prompt." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the new WhatsApp systemPrompt resolution hierarchy and composition that captures the core change and would match similar PRs/issues." + }, + "usage": { + "input_tokens": 2490, + "output_tokens": 545 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before agent processing, and WhatsApp group policy resolution could ignore account-scoped config (causing allowlist/require-mention checks to fail).", + "solution_summary": "Append processing is enabled only for WhatsApp group messages, and group policy resolution now propagates accountId and uses account-scoped groupAllowFrom/allowFrom fallbacks so account-scoped WhatsApp settings apply; DM append history remains skipped.", + "maintainer_signal_summary": "Change touches WhatsApp inbound and group-activation code (src/web/inbound/monitor.ts, src/web/auto-reply/monitor/group-activation.ts), was validated against a live OpenClaw WhatsApp deployment with trace logs showing before/after behavior, adds targeted tests for account-scoped allowlist fallback, and makes no config/schema/debug/logging or unrelated channel changes.", + "dedupe_summary": "Enable messages.upsert(type=\"append\") processing for WhatsApp groups and use accountId to apply account-scoped groupAllowFrom/allowFrom when resolving WhatsApp group policy defaults." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core change—enabling append processing for WhatsApp groups and using account-scoped accountId for group policy resolution—without extraneous boilerplate." + }, + "usage": { + "input_tokens": 2042, + "output_tokens": 491 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Canvas (A2UI) returned 401 when accessed via Tailscale Serve because the gateway treated requests from the trusted proxy as unauthenticated: resolveClientIp() returned undefined when the proxy sent no forwarding headers, and isLocalDirectRequest() did not fall back to the raw socket address when XFF resolved to a non-loopback Tailscale IP.", + "solution_summary": "Add socket-address fallback in resolveClientIp() when no forwarding headers are present; have isLocalDirectRequest() check req.socket.remoteAddress when allowRealIpFallback is enabled; gate .ts.net host acceptance behind the existing allowTailscale flag and thread auth.allowTailscale through callers; update docs and tests.", + "maintainer_signal_summary": "Changes touch gateway auth/net/server code and tests (src/gateway/{auth.ts,net.ts,server-*.ts}, session-kill-http) and docs; tests added and passing. Behavior change only when no XFF/X-Real-IP headers are present or when allowRealIpFallback is enabled; allowTailscale defaults to true for tailscale.mode = \"serve\" to preserve existing setups. Risk: different callers may now see socket IP when proxies omit forwarding headers; mitigation and docs provided.", + "dedupe_summary": "Canvas returned 401 behind Tailscale Serve because resolveClientIp returned undefined for direct trusted-proxy requests without XFF and isLocalDirectRequest didn’t fall back to socket.remoteAddress; fix adds socket fallback and gates .ts.net host acceptance behind allowTailscale." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that states the root cause (no socket fallback causing 401 behind Tailscale Serve) and the precise fixes (socket.remoteAddress fallback and gating .ts.net via allowTailscale), so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 1881, + "output_tokens": 756 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "checkInboundAccessControl computed an isSelfChat heuristic but never used it as a guard, allowing non-self WhatsApp messages (group messages, DMs from strangers, broadcasts) to pass through because groupPolicy defaulted to \"open\", violating self-chat isolation when selfChatMode was intended to be absolute.", + "solution_summary": "Introduce an early-return guard in checkInboundAccessControl that rejects all non-self messages when account.selfChatMode === true; the guard runs before group/DM policy evaluation and intentionally uses the explicit config flag (not the isSelfChat heuristic) to avoid blocking teammates in mixed allowFrom lists.", + "maintainer_signal_summary": "Guard is placed before group and DM policy resolution and preserves isSelfChat in the result for UI/logging; auto-detected self-chat via heuristic is not hard-blocked by design; tests pass and the change can be reverted by removing the early-return guard.", + "dedupe_summary": "Explicit selfChatMode true did not block non-self WhatsApp messages because isSelfChat was computed but unused and groupPolicy defaulted to \"open\"; fix adds an early-return that rejects non-self messages when account.selfChatMode === true." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the bug (isSelfChat computed but unused, groupPolicy defaulting to open) and the exact fix (early-return rejecting non-self messages when account.selfChatMode === true), so it will cluster correctly with similar changes." + }, + "usage": { + "input_tokens": 1596, + "output_tokens": 735 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "With groupPolicy: \"allowlist\", group messages from senders not in groupAllowFrom are dropped at the access-control layer (allowed: false → continue) and never stored for context, so the agent cannot see those messages in its \"[Chat messages since your last reply]\" context block and documented mention-gating behavior is not met.", + "solution_summary": "Introduce an opt-in groupContextFromAll flag (top-level and per-account, per-account wins) defaulting to false; when enabled, access-control returns storeForContext for non-allowlisted group senders, inbound monitor marks them as contextOnly (skips read receipts) and records them in group history as pending context without triggering replies; added types, schema, and account config changes while preserving backward compatibility.", + "maintainer_signal_summary": "Changes touch src/web/inbound/access-control.ts, monitor.ts, types.ts, auto-reply/on-message.ts, config types and zod schema, and accounts resolution. Potential risks: allowlist normalization/matching may misclassify senders, and the new context-only early path bypasses some existing group gating/state updates, which could cause subtle inconsistencies.", + "dedupe_summary": "Add groupContextFromAll option to store non-allowlisted WhatsApp group messages as pending context (contextOnly) instead of dropping them when groupPolicy is allowlist." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core change—new groupContextFromAll option to store non-allowlisted WhatsApp group messages as pending context instead of dropping them—without boilerplate." + }, + "usage": { + "input_tokens": 1613, + "output_tokens": 799 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "sessions_spawn always created a new random-UUID session for each sub-agent spawn, preventing reuse of conversation history and enabling workarounds that bypass allowlists.", + "solution_summary": "Add an optional sessionKey parameter so sub-agents can run in a deterministic session. Short keys map to agent:{agentId}:subagent:{key}; fully-qualified keys containing \":subagent:\" are validated to match the targetAgentId to prevent cross-agent session injection; omission falls back to crypto.randomUUID(). Includes four tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch rejection.", + "maintainer_signal_summary": "Blocking test import: a test file imports a non-existent createOpenClawTools from ./openclaw-tools.js instead of the correct ./clawdbot-tools.js, causing compile failure. Case-sensitivity bug: embedded agentId extracted from fully-qualified keys is not normalized/lowercased while targetAgentId is normalized, causing false mismatch rejections. Inconsistent error handling: cross-agent mismatch uses throw new Error(...) while other security checks return jsonResult(...), producing different error surfaces.", + "dedupe_summary": "sessions_spawn now accepts an optional sessionKey to reuse deterministic sub-agent sessions; short keys map to agent:{agentId}:subagent:{key}, fully-qualified keys are validated to match targetAgentId to prevent cross-agent session injection, otherwise falls back to crypto.randomUUID()." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change (optional sessionKey for deterministic sub-agent sessions), mapping rules, validation to prevent cross-agent injection, and fallback behavior without template noise." + }, + "usage": { + "input_tokens": 1666, + "output_tokens": 711 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) via the generic edit path calls editMessageText and fails with 400: \"there is no text in the message to edit\", causing message edits to silently error and leave the UI inconsistent.", + "solution_summary": "editMessageTelegram now detects that error via a NO_TEXT_IN_MESSAGE_RE regex, retries the edit with editMessageCaption for captionable media, routes buttons-only updates to editMessageReplyMarkup, preserves HTML parse fallback, and treats MESSAGE_NOT_MODIFIED as success; no public API or config changes.", + "maintainer_signal_summary": "Changes live in src/telegram/send.ts: added NO_TEXT_IN_MESSAGE_RE and retry logic from editMessageText → editMessageCaption, plus direct editMessageReplyMarkup path for buttons-only updates; six unit tests added covering retry, buttons-only, error propagation, and MESSAGE_NOT_MODIFIED handling; backward compatible and only affects the Telegram adapter; risk: regex may need updates if Telegram changes error wording.", + "dedupe_summary": "Telegram editMessageText returns 400 'there is no text in the message to edit' for media messages; fix retries with editMessageCaption and uses editMessageReplyMarkup for buttons-only updates." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that states the specific Telegram API error and the implemented retry/fallback (editMessageCaption and editMessageReplyMarkup), with no boilerplate or extraneous details." + }, + "usage": { + "input_tokens": 1547, + "output_tokens": 711 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "PR #24962 hardcoded disableBlockStreaming:true for WhatsApp to address a reasoning-content leak, which unintentionally disabled block streaming entirely (messages were batched and sent at the end) and ignored per-account and global block streaming config.", + "solution_summary": "Changed the deliver callback to only suppress tool payloads and made disableBlockStreaming driven by per-account channels.whatsapp.blockStreaming with a fallback to agents.defaults.blockStreamingDefault; upstream reasoning suppression remains in place.", + "maintainer_signal_summary": "Reasoning/thinking content is still filtered upstream by shouldSuppressReasoningPayload; tool payloads remain suppressed; tests (including live WhatsApp verification and inbound contract tests) pass; default behavior unchanged unless explicitly configured.", + "dedupe_summary": "WhatsApp block streaming was hardcoded off (disableBlockStreaming:true), ignoring channels.whatsapp.blockStreaming and agents.defaults.blockStreamingDefault; this change restores config-driven block streaming and limits deliver suppression to tool payloads while keeping reasoning suppression upstream." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core bug (hardcoded disableBlockStreaming for WhatsApp), the config-driven restoration, and the narrowed deliver suppression to tool payloads while noting reasoning filtering remains upstream." + }, + "usage": { + "input_tokens": 1539, + "output_tokens": 634 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Sending videos larger than the previous 5MB default caused the Telegram gateway to hang: fetchRemoteMedia hit a content-length > maxBytes and closeDispatcher (undici Agent.close) deadlocked when the response body was unconsumed, leaving the gateway unresponsive and previously dropping messages when media failed.", + "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher to timeout and fall back to destroy to avoid Agent.close deadlocks, added timeouts and retry logic to getFile and downloads, made media download errors non-fatal so text/captions are forwarded, and extended UI metadata stripping for inbound media paths.", + "maintainer_signal_summary": "Changes touch src/infra/net/ssrf.ts and src/telegram/bot.ts; default mediaMaxMb is now 20 but remains configurable via channels.telegram.mediaMaxMb; no SSRF policy, media storage, or agent-side processing changes; strip-inbound-meta tests updated and pass.", + "dedupe_summary": "Telegram gateway hung on video messages >5MB due to undici Agent.close deadlock when response body was unconsumed; fix increases channels.telegram.mediaMaxMb to 20, adds timeouts/retries for media download, makes media errors non-fatal, and forces Agent.close to timeout then destroy." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and technical summary that states the root cause (undici Agent.close deadlock) and the specific fixes (increase mediaMaxMb, timeouts/retries, non-fatal media errors, forced Agent.close destruction), with no boilerplate — ideal for deduplication." + }, + "usage": { + "input_tokens": 1635, + "output_tokens": 897 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "Telegram document attachments sent without caption were always represented as \"\" in the agent-facing inbound body, mislabeling documents (PDF/DOCX/etc.) as images and causing agents to respond as if no document was received.", + "solution_summary": "Changed resolveTelegramInboundBody to use the precomputed type-aware placeholder (e.g. \"\") instead of a hardcoded \"\" for non-audio media; added unit tests covering placeholders for all media types and a test verifying document messages trigger getFile.", + "maintainer_signal_summary": "Scope limited to the agent-facing fallback body text in Telegram integration; media download, understanding pipeline, and file extraction unchanged. Tests added in extensions/telegram/src/bot/helpers.test.ts and extensions/telegram/src/bot.create-telegram-bot.test.ts. Risk: downstream code that pattern-matches only \"\" might be affected, but this was an edge-case fallback and the change is more correct.", + "dedupe_summary": "Telegram inbound-body fallback used hardcoded \"\" for non-audio media, causing documents to be labeled as images; fixed to use the type-aware placeholder so document attachments produce \"\"." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the bug (hardcoded placeholder) and the fix (use type-aware placeholder producing ), with relevant technical terms and no boilerplate." + }, + "usage": { + "input_tokens": 1462, + "output_tokens": 637 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "MEDIA_MAX_BYTES was hardcoded to 5 MB in src/media/store.ts, causing files larger than 5 MB to fail to stage into sandboxed agents and making common file types unusable in sandboxed workflows.", + "solution_summary": "Introduced a new media.maxBytes config field, added getMediaMaxBytes()/setMediaMaxBytes() accessors and wired gateway boot to read and apply the config, updated error messages to show the configured limit; default remains 5 MB and schema caps values at 500 MB.", + "maintainer_signal_summary": "All relevant tests pass (56 tests across four files). Backwards compatible: MEDIA_MAX_BYTES exported (deprecated) and default preserved. Schema enforces 500 MB max; fixed previous issues with unset config and MB formatting; risk of disk fill noted and mitigated by TTL and schema cap.", + "dedupe_summary": "Make MEDIA_MAX_BYTES configurable via media.maxBytes; replace hardcoded 5MB staging limit with a runtime-configurable limit (default 5MB, maximum 500MB) so sandboxed agents can accept larger files." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Clear, concise technical summary that captures the core change (replace hardcoded 5MB staging limit with media.maxBytes config and default/maximum values) without template noise, so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 1583, + "output_tokens": 476 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "When Telegram media download fails due to exceeding size limits or a Telegram API error, the handler returned early and the entire message (including caption/text) was silently dropped; the agent received no ack and the user got no indication their file was lost.", + "solution_summary": "On download failure, extract file metadata (name, size, type) from the Telegram message object and inject a metadata stub into msg.text instead of returning early, allowing the message to continue through the normal pipeline so the agent sees the file and reason for failure.", + "maintainer_signal_summary": "Change implemented in src/telegram/bot-handlers.ts: buildFileMetadataStub populates msg.text on error paths for files over mediaMaxMb or Telegram's 20MB getFile limit; normal media download, media groups, sticker handling, and outbound sending are unchanged. No new network calls or permissions; risk is mutating msg.text only on previously-dropped messages and can be reverted by reverting the single commit to bot-handlers.ts.", + "dedupe_summary": "Telegram media download failures (file too large or API error) previously caused the message to be dropped; now file metadata stub is injected into msg.text so the agent receives the file and failure reason." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: states the bug (Telegram media download failures dropped messages) and the exact fix (inject file metadata stub into msg.text so the agent sees the file and failure reason), which will match similar issues." + }, + "usage": { + "input_tokens": 1425, + "output_tokens": 634 + } + }, + { + "thread_id": 5796, + "number": 24117, + "kind": "pull_request", + "title": "feat(cli): add --session-key targeting for openclaw agent", + "summary": { + "problem_summary": "The openclaw agent CLI lacked a stable targeting option, causing CLI-triggered deliveries and callback automation to resolve to the agent main session because sessionId is UUID-based and --to or --agent are unreliable in some environments.", + "solution_summary": "Added a new --session-key CLI flag, extended AgentCliOpts with sessionKey, forwarded it into resolveSessionKeyForRequest and the gateway call, updated help/validation text, and added tests to verify parsing and forwarding. Session resolution precedence is now session-key > session-id > to > agent.", + "maintainer_signal_summary": "Safe, backward-compatible change touching src/cli/program/register.agent.ts and src/commands/agent-via-gateway.ts, adds tests for CLI parsing and gateway parameter forwarding, minimal risk and straightforward to review and merge.", + "dedupe_summary": "Add --session-key flag to openclaw agent CLI; forward sessionKey through AgentCliOpts into resolveSessionKeyForRequest so session targeting uses session-key over session-id/to/agent." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core change—adding a --session-key CLI flag and plumbing it through AgentCliOpts to resolveSessionKeyForRequest to prioritize session-key targeting—without extraneous boilerplate." + }, + "usage": { + "input_tokens": 1397, + "output_tokens": 531 + } + }, + { + "thread_id": 766, + "number": 11611, + "kind": "pull_request", + "title": "feat: separate group-level allowlist from sender-level command authorization", + "summary": { + "problem_summary": "With groupPolicy set to \"allowlist\", the system currently gates all group interactions (chat and slash commands) by sender phone number via groupAllowFrom, preventing: allowing specific groups while letting anyone chat, restricting slash commands to owners only, and blocking spam groups while keeping approved groups open.", + "solution_summary": "When groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access is gated by the groups JID allowlist (only listed groups accepted) while anyone in an approved group may chat; groupAllowFrom continues to control slash-command authorization. If no groups mapping is present, behavior falls back to the legacy sender-based filtering.", + "maintainer_signal_summary": "Changes localized to src/web/inbound/access-control.ts (use resolveChannelGroupPolicy), updated JSDoc in src/config/types.whatsapp.ts, and four new Vitest tests for group allowlist behavior; depends on PR #3326 to require explicit groupPolicy: \"allowlist\" when using groups config and includes a minor doc/comment update.", + "dedupe_summary": "WhatsApp: groupPolicy \"allowlist\" now uses channels.whatsapp.groups JID mapping as the group-level allowlist (allows any participant to chat in approved groups) while groupAllowFrom still limits slash commands; falls back to sender-based filtering when no groups mapping is configured." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it captures the core change to use the channels.whatsapp.groups JID mapping as the group-level allowlist, the resulting open-chat behavior, that groupAllowFrom still gates slash commands, and the sender-filtering fallback." + }, + "usage": { + "input_tokens": 1447, + "output_tokens": 558 + } + }, + { + "thread_id": 17924, + "number": 46425, + "kind": "pull_request", + "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", + "summary": { + "problem_summary": "Telegram's configured mediaMaxMb limit was ignored for outbound reply messages because loadWebMedia in the reply delivery path received no maxBytes and fell back to the default 100 MB, so smaller operator limits had no effect.", + "solution_summary": "Propagated mediaMaxBytes through the Telegram outbound reply delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia receives the configured maxBytes; added unit tests and kept behavior unchanged when unset.", + "maintainer_signal_summary": "Direct-send path in send.ts was already correct; this change only threads mediaMaxBytes through reply and native-commands delivery paths, is backward compatible (mediaMaxBytes optional), and unit tests covering the change pass.", + "dedupe_summary": "Telegram outbound reply delivery ignored telegramAccounts.mediaMaxMb (loadWebMedia got no maxBytes and defaulted to 100MB); mediaMaxBytes is now threaded through bot.ts, bot-message.ts, bot-message-dispatch.ts, delivery.replies.ts and bot-native-commands.ts so replies enforce the configured limit." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the bug (reply delivery ignored mediaMaxMb) and the fix (threaded mediaMaxBytes through the listed Telegram outbound delivery files) with no template noise, so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 1448, + "output_tokens": 732 + } + }, + { + "thread_id": 5712, + "number": 37050, + "kind": "pull_request", + "title": "fix(telegram): add HTML parse fallback for media captions", + "summary": { + "problem_summary": "Telegram media send functions (photo, document, video, animation, audio, voice, video note) lacked the HTML-parse fallback used by text messages, so captions with invalid HTML/entities caused the whole media send to fail with a 400 \"can't parse entities\" error.", + "solution_summary": "Wrapped the media-send callback in withTelegramHtmlParseFallback so on an HTML parse error the send is retried without parse_mode and with the raw caption; this applies to all media types and a unit test verifies the initial failure and subsequent retry behavior. No changes were made to text message, edit message, thread fallback, or caption-splitting logic.", + "maintainer_signal_summary": "Change made in src/telegram/send.ts; added unit test 'retries media caption as plain text on HTML parse error' and all tests pass. Backward-compatible; revert by removing the fallback wrapper; risk is only that fallback sends raw text instead of rendered HTML.", + "dedupe_summary": "Telegram media sends (sendPhoto/sendDocument/sendVideo/sendAnimation/sendAudio/sendVoice/video note) failed with 400 \"can't parse entities\" for captions containing invalid HTML; now retries by removing parse_mode and sending caption as plain text." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core bug and fix naming affected APIs and error and omitting boilerplate, so it will cluster correctly with similar reports." + }, + "usage": { + "input_tokens": 1322, + "output_tokens": 703 + } + }, + { + "thread_id": 21769, + "number": 55280, + "kind": "pull_request", + "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", + "summary": { + "problem_summary": "WhatsApp had block streaming effectively disabled and a delivery race in the block-reply pipeline that could suppress both intermediate blocks and the final reply. disableBlockStreaming was hardcoded and its assignment used undefined which prevented activation, and onBlockReply used a sync enqueue so the pipeline marked sentContentKeys before actual delivery.", + "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming; fix disableBlockStreaming assignment; add blockStreamingCoalesceDefaults; implement dispatcher.sendBlockReplyAsync and await it in onBlockReply so sentContentKeys are only marked after confirmed delivery; update tests for streaming-on and streaming-off paths.", + "maintainer_signal_summary": "Key changes: process-message.ts now resolves account blockStreaming and fixes disableBlockStreaming assignment; shared.ts adds coalesce defaults; reply-dispatcher.ts adds sendBlockReplyAsync with per-item settlement; dispatch-from-config.ts awaits sendBlockReplyAsync; tests updated and new cases added to cover streaming enabled/disabled. This fixes the root cause of silent turns by ensuring block delivery is confirmed before marking sent.", + "dedupe_summary": "WhatsApp block streaming race: onBlockReply used sync sendBlockReply causing pipeline to mark sentContentKeys before confirmed delivery and suppress final replies; fixed by making blockStreaming configurable, correcting disableBlockStreaming handling, and adding sendBlockReplyAsync with awaiting of confirmed delivery." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that identifies the core race condition in WhatsApp block-reply handling, the incorrect disableBlockStreaming handling, and the implemented fix (awaiting delivery via sendBlockReplyAsync), so it would cluster with similar issues." + }, + "usage": { + "input_tokens": 1235, + "output_tokens": 845 + } + }, + { + "thread_id": 8098, + "number": 25543, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked a group-level allowlist option; groups could not be explicitly allowed by ID and thus were still subject to sender-level groupAllowFrom checks and could not run control commands.", + "solution_summary": "Added channels.signal.groups. config and SignalGroupConfig type, extended Zod schema, wired resolveChannelGroupPolicy into the Signal event handler to allow group-level bypass for message delivery and command authorization, and updated docs with examples.", + "maintainer_signal_summary": "Implementation follows existing patterns used by other channels, preserves backward compatibility via hasGroupAllowFrom, TypeScript compilation passes, and the change was manually tested; safe to merge.", + "dedupe_summary": "Add channels.signal.groups. allowlist to bypass sender-level groupAllowFrom and permit control commands for explicitly allowed Signal groups." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary describing the new Signal groups allowlist that bypasses sender-level checks and enables command authorization for allowed groups, which is exactly the core change for deduplication." + }, + "usage": { + "input_tokens": 1177, + "output_tokens": 491 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal was the only channel missing group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter and group IDs never matched sender IDs.", + "solution_summary": "Added a groups config to SignalAccountSchemaBase and updated Signal's event handler to call resolveChannelGroupPolicy before falling back to sender-level groupAllowFrom; added four tests and verified existing tests pass.", + "maintainer_signal_summary": "Signal lacked the groups config and the resolveChannelGroupPolicy access check used by other channels; fix adds the groups schema and invokes resolveChannelGroupPolicy in Signal's group access path so group IDs in channels.signal.groups. permit group messages.", + "dedupe_summary": "Signal lacked group-level allowlist support (channels.signal.groups.); groupAllowFrom is sender-level so group IDs never matched; added groups config and resolveChannelGroupPolicy call to allow group-level allowlisting." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the bug (missing group-level allowlist for Signal due to misuse of sender-level groupAllowFrom) and the fix (added groups config and resolveChannelGroupPolicy), so it will match similar issues precisely." + }, + "usage": { + "input_tokens": 977, + "output_tokens": 504 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "WhatsApp messages are being batched because process-message.ts hardcodes disableBlockStreaming: true, causing intermediate agent blocks to accumulate and be sent as one final payload.", + "solution_summary": "Replace the hardcoded disableBlockStreaming with a config-driven resolveWhatsAppBlockStreamingEnabled helper, register blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in the WhatsApp dock, and fix queuedFinal logic so intermediate block payloads count as delivered; tests updated to cover enabled/disabled behavior.", + "maintainer_signal_summary": "When channels.whatsapp.blockStreaming is true, intermediate blocks are delivered as separate WhatsApp messages immediately; when false or omitted, behavior remains backward-compatible with a single final reply. Change aligns WhatsApp behavior with Telegram/Line.", + "dedupe_summary": "Respect channels.whatsapp.blockStreaming instead of hardcoded disableBlockStreaming:true so intermediate blocks are delivered as separate WhatsApp messages; fix queuedFinal marking and register the capability/default." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core change—making WhatsApp block streaming config-driven, fixing queuedFinal delivery behavior, and registering the capability—without template noise." + }, + "usage": { + "input_tokens": 727, + "output_tokens": 692 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v4-terse.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v4-terse.json new file mode 100644 index 0000000..49df43c --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v4-terse.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v4-terse", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "Extract the core topic from this GitHub issue or PR for duplicate clustering. Ignore all template boilerplate, testing notes, checklists, environment details, and reproduction steps. Focus only on WHAT the issue/change is about technically.\n\nReturn JSON: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Plain text, 1-2 sentences each. Be extremely concise — every word in dedupe_summary must add signal for finding duplicates.", + "timestamp": "2026-03-30T17:05:31.524Z", + "aggregate": { + "avg_score": 4.47, + "avg_signal_density": 4.5, + "boilerplate_count": 1, + "boilerplate_pct": 3, + "would_cluster_correctly_pct": 100, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 41983, + "total_output_tokens": 27511 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "channels.discord.proxy only proxies the Discord WebSocket gateway; startup REST API calls (deploy commands, fetch bot identity) and guild resolve do not use the proxy, causing REST timeouts and guild channels to be non-functional behind an HTTP proxy.", + "solution_summary": "Ensure the configured proxy is applied to the undici dispatcher used for all Discord REST calls and guild resolution during provider startup (or add a global proxy setting), e.g., set a ProxyAgent as the global dispatcher and add retry logic for guild resolve.", + "maintainer_signal_summary": "Regression: WebSocket connects via proxy but REST calls time out with 'fetch failed' on startup; Node's undici fetch ignores HTTP_PROXY; adding a ProxyAgent fixes REST but guild resolve still uses an unproxied code path; reproduces on OpenClaw 2026.3.13 with Node 25.", + "dedupe_summary": "discord proxy websocket-only rest-api guild-resolve fails undici fetch no-proxy ProxyAgent" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise set of technical keywords that capture the core issue (proxying only WebSocket, REST API/guild resolve failures, undici/ProxyAgent), but it's fragmented and could be a clearer single-sentence summary." + }, + "usage": { + "input_tokens": 2185, + "output_tokens": 749 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "Discord outbound REST calls (sending messages/attachments) in OpenClaw ignore channels.discord.proxy because the RequestClient fetch doesn't use the configured proxy, causing 'fetch failed' errors in proxy-restricted environments.", + "solution_summary": "Update RequestClient/fetch to honor channels.discord.proxy and add proxy support (HTTP/SOCKS/tunneling) for REST API and attachment transfers so all Discord traffic routes through the configured proxy.", + "maintainer_signal_summary": "User set channels.discord.proxy=http://127.0.0.1:7890/; WebSocket gateway receives messages (proxy working) but REST sends fail with 'fetch failed'; logs show rest proxy enabled but REST not using proxy; OpenClaw 2026.2.26 on restricted-region network.", + "dedupe_summary": "Discord REST ignores channels.discord.proxy; RequestClient fetch lacks proxy support." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: states that Discord REST calls ignore channels.discord.proxy because the underlying RequestClient fetch has no proxy support, which clearly captures the core bug for deduplication." + }, + "usage": { + "input_tokens": 867, + "output_tokens": 623 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "When Discord is configured behind an HTTP proxy, OpenClaw successfully logs in (gateway/WebSocket) and fetches applicationId but some startup REST calls—native command deployment (/applications/.../commands) and bot identity fetch—fail with 'fetch failed'.", + "solution_summary": "Unify proxy handling by routing all Discord REST requests through the same proxy-aware HTTP client/dispatcher used by the gateway: propagate channels.discord.proxy to deploy/identity request code paths or replace divergent fetch implementations, and add tests/logging to verify proxy usage.", + "maintainer_signal_summary": "Startup logs show 'rest proxy enabled' and 'gateway proxy enabled', applicationId fetch succeeded, but deploy-rest PUT and fetch-bot-identity both error with 'fetch failed' while the provider still reports 'logged in to discord', indicating inconsistent proxy application across transport paths.", + "dedupe_summary": "discord proxy fetch-failed native-command-deploy bot-identity-fetch inconsistent-proxy REST-vs-gateway /applications/.../commands channels.discord.proxy logged-in" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused keywords that capture the core issue: Discord REST calls (native command deploy and bot identity fetch) failing behind channels.discord.proxy while the gateway/login works, with no template noise." + }, + "usage": { + "input_tokens": 1013, + "output_tokens": 754 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "OpenClaw logs the Discord channel proxy as enabled but discord.js REST requests fail with 'fetch failed' and the WebSocket gateway never establishes under WSL2, despite the proxy working for curl.", + "solution_summary": "Ensure OpenClaw actually applies the configured HTTP proxy to the Discord.js REST client (or sets HTTP(S)_PROXY / an appropriate agent/undici proxy option) so fetch and gateway connections use the proxy.", + "maintainer_signal_summary": "Logs show 'rest proxy enabled' and 'gateway proxy enabled' yet REST calls error 'fetch failed' and gatewayConnected=false; reproducible on WSL2 with proxy 127.0.0.1:10808 while curl succeeds and Feishu channel works.", + "dedupe_summary": "openclaw discord proxy-not-applied fetch-failed discord.js WSL2 gatewayConnected=false rest-api" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Captures the core issue (Discord proxy not being applied causing REST fetch failures under WSL2 with discord.js) but is terse and contains a noisy token ('gatewayConnected=false') that isn't necessary for deduplication." + }, + "usage": { + "input_tokens": 850, + "output_tokens": 777 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "Discord REST API HTTP requests ignore configured proxy (channels.discord.proxy and HTTP_PROXY), causing 'TypeError: fetch failed' when sending messages while WebSocket and other channels work.", + "solution_summary": "Route Discord REST API calls through the existing proxy-aware fetch/SSRF guard used for web_search/web_fetch so they honor channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY.", + "maintainer_signal_summary": "Repro on OpenClaw 2026.2.26 (Node v25.5.0, macOS); WebSocket receives events, curl via proxy to Discord API succeeds, but bot reply REST calls fail with 'TypeError: fetch failed'; changelog notes web tools proxy fix excluded Discord REST.", + "dedupe_summary": "discord REST API ignores proxy; channels.discord.proxy/HTTP_PROXY not applied; fetch failed on send message; WebSocket ok; web_search proxy fix missing for discord" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary concisely captures that Discord REST calls ignore the configured proxy and fail with fetch errors while WebSocket works, but adds a possibly incorrect/unclear mention of a 'web_search' proxy fix which is minor noise." + }, + "usage": { + "input_tokens": 873, + "output_tokens": 516 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "The CLI command openclaw agent --channel discord --to delivers messages to agent:*:main instead of the live Discord channel/thread because the session resolver does not search by delivery context and Discord targets are not normalized, and the command path fails to propagate channel/account context.", + "solution_summary": "Manually patched compiled dist files to add session lookup by delivery context (channel+to+accountId), normalize Discord targets, pass channel and replyAccount/accountId through the command path, and use buildAgentMainSessionKey for correct lookup.", + "maintainer_signal_summary": "High-risk manual fixes applied to dist/gateway-cli-*.js, dist/compact-*.js, and dist/register.agent-*.js; change must be implemented in source, tested, and preserved across updates (add revalidation script/tests).", + "dedupe_summary": "CLI agent misroutes to agent:*:main; session resolver missing delivery-context lookup (channel+to+accountId); Discord target normalization bug; patched dist/*.js" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and focused: identifies CLI misrouting, missing delivery-context lookup (channel+to+accountId), Discord target normalization bug, and patched dist files—no template noise and uses specific technical terms for correct clustering." + }, + "usage": { + "input_tokens": 800, + "output_tokens": 699 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions: Discord-thread messages had volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) re-injected every turn causing context bloat/degradation; CLI agent commands routed to the default main session instead of the live thread-bound session.", + "solution_summary": "Prevent repeated injection of Discord-thread volatile metadata and update CLI/gateway session resolution to look up sessions by actual delivery context (normalized channel + to + accountId), passing channel and replyAccount/accountId and fixing resolveAgentMainSessionKey misuse; changes applied to compiled dist bundles.", + "maintainer_signal_summary": "Manual patches applied to dist/*.js (pi-embedded, gateway-cli, compact, register.agent) and openclaw-gateway.service restarted; verification shows threads no longer repeat metadata and CLI routes correctly; note patches in openclaw/dist/ will be lost on update—reapplication script required.", + "dedupe_summary": "discord threads volatile metadata re-injection repeated metadata per-turn message_id reply_to_id timestamp \"Thread starter\" \"Chat history\" CLI routing wrong session session resolver channel to accountId compiled dist bundles manual patch" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary that captures both core regressions—per-turn re-injection of volatile Discord thread metadata and incorrect CLI session routing/resolver behavior—using specific technical terms suitable for deduplication." + }, + "usage": { + "input_tokens": 719, + "output_tokens": 577 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "The sandbox media staging cap is hardcoded as const MEDIA_MAX_BYTES = 5 * 1024 * 1024 in dist/store-*.js, so files larger than 5MB are rejected and cannot be overridden via openclaw.json.", + "solution_summary": "Make MEDIA_MAX_BYTES configurable (e.g., read tools.media.maxBytes from openclaw.json or honor per-type limits) with a sensible default, and update distributed build files to use the configured value.", + "maintainer_signal_summary": "Regression introduced before v2026.3.7 and present in v2026.3.8; occurs in distributed dist/store-*.js for npm global installs; workaround is manual sed patch of dist files and gateway restart; reproduces when sandbox.mode is non-main.", + "dedupe_summary": "MEDIA_MAX_BYTES hardcoded 5MB sandbox staging limit not configurable tools.media.maxBytes openclaw.json dist/store-*.js regression v2026.3.7 v2026.3.8 npm-global" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "The summary concisely identifies the core issue — a hardcoded 5MB MEDIA_MAX_BYTES sandbox staging limit and relevant config/file locations — but includes noisy version tags and an unrelated 'npm-global' token that aren't helpful for deduplication." + }, + "usage": { + "input_tokens": 766, + "output_tokens": 691 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "OpenClaw's Discord REST API requests fail with 'fetch failed' even though rest proxy is configured and enabled; WebSocket gateway and curl via the same proxy work fine.", + "solution_summary": "Ensure the REST client honors the configured proxy—pass a proxy agent to the Node fetch/undici client or honor HTTP(S)_PROXY env vars and implement HTTPS CONNECT proxy handling for Discord API requests.", + "maintainer_signal_summary": "Logs report 'rest proxy enabled' but REST operations (deploy commands, fetch bot identity, send messages) error with 'fetch failed' while gateway/WebSocket and Telegram work and curl with the same proxy succeeds, indicating proxy not applied to REST fetch.", + "dedupe_summary": "discord REST fetch failed proxy-enabled OpenClaw undici fetch not-using-proxy https-connect" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific keywords capture the core problem (Discord REST requests failing despite proxy configuration, implicating OpenClaw/undici proxy usage and https-connect) with no boilerplate or noise." + }, + "usage": { + "input_tokens": 711, + "output_tokens": 820 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "Discord REST API requests (via @buape/carbon's RequestClient) ignore openclaw.json channels.discord.proxy and call globalThis.fetch without a proxy-aware dispatcher, causing REST calls, attachments, and memorySearch to bypass the configured proxy while the WebSocket gateway respects it.", + "solution_summary": "Update @buape/carbon's RequestClient to accept an optional dispatcher/proxy option (or otherwise use an undici proxy-aware dispatcher) and have OpenClaw pass channels.discord.proxy so REST requests, attachments, and memorySearch use the configured proxy.", + "maintainer_signal_summary": "Repro on Node.js 22; WebSocket gateway already uses proxy; REST client uses native fetch and ignores HTTP_PROXY/HTTPS_PROXY unless --use-env-proxy (which breaks WebSocket). Workaround is setting undici EnvHttpProxyAgent via preload to setGlobalDispatcher; recommended fix is plumbing proxy into RequestClient.", + "dedupe_summary": "Discord REST (@buape/carbon RequestClient) ignores channels.discord.proxy; uses globalThis.fetch without undici dispatcher on Node.js 22; attachments/memorySearch bypass proxy." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core bug (RequestClient/REST calls ignoring channels.discord.proxy and using globalThis.fetch/undici-less path, plus attachments/memorySearch bypassing proxy) but includes a Node.js version mention that is unnecessary noise." + }, + "usage": { + "input_tokens": 720, + "output_tokens": 808 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "Under sustained load OpenClaw experiences rising per-turn latency and serialization due to unbounded context token growth (writer ~42k, reasoning ~90k), long-running turns blocking Discord lanes, and typing indicators remaining until TTL.", + "solution_summary": "Applied configuration limits: reduce concurrency (acp.maxConcurrent 12→6, agents.maxConcurrent 3→2, subagents 8→4), set agents.defaults.contextTokens=80000, increase channels.discord.eventQueue.listenerTimeout 120000→300000, and improved patching script coverage.", + "maintainer_signal_summary": "Gateway restarted to active (running) and no new \"lane wait exceeded\" or \"typing TTL reached\" errors observed; patch script at openclaw-rootfix-v2026.3.11.sh:73; recommendation to expose adaptive/tunable token and concurrency limits.", + "dedupe_summary": "context-token-growth, long-turn lane-blocking, discord lane serialization, typing-TTL-stuck, concurrency-saturation, eventQueue-listenerTimeout, contextTokens=80000, reduced-maxConcurrent" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise, technical keywords capture the core issues (context token growth, lane blocking/serialization, typing TTL, concurrency, event queue timeout) but is a bit fragmentary and includes a specific token count/config tweak that is unnecessary noise." + }, + "usage": { + "input_tokens": 654, + "output_tokens": 766 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "Discord threads in v2026.3.11 repeatedly re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, causing context contamination, token growth, and prompt-cache invalidation.", + "solution_summary": "Manually patched compiled dist files to stop per-turn metadata injection and added an isNewSession guard (dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js), and replaced resolveAgentMainSessionKey with buildAgentMainSessionKey; recommend applying the fix in source and adding post-update validation.", + "maintainer_signal_summary": "HIGH regression risk: fixes made to compiled/dist files will be lost on update; affected artifacts: dist/pi-embedded-D6PpOsxP.js (metadata injection) and dist/compact-1mmJ_KWL.js (session handler/isNewSession, session-key change). Upstream source fix and test needed to prevent prompt-cache bust in long Discord threads.", + "dedupe_summary": "Discord thread metadata re-injected every turn; message_id reply_to_id timestamp \"Thread starter\" \"Chat history\" prompt-cache-bust dist/pi-embedded-D6PpOsxP.js dist/compact-1mmJ_KWL.js isNewSession compiled-dist patch" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core bug (Discord thread volatile metadata re-injected each turn), lists affected fields and compiled files, and contains no template boilerplate." + }, + "usage": { + "input_tokens": 666, + "output_tokens": 783 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "In v2026.3.11 Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, token bloat and prompt cache busting.", + "solution_summary": "Applied manual patch to dist/pi-embedded-D6PpOsxP.js to stop reinjecting that metadata after the first turn; recommend committing a source-level fix and a post-update revalidation script to avoid regressions.", + "maintainer_signal_summary": "Issue is in compiled artifact dist/pi-embedded-D6PpOsxP.js in OpenClaw v2026.3.11; high regression risk because changes were made to dist—fix must be propagated to source and preserved across updates.", + "dedupe_summary": "Discord thread metadata reinjected every turn, message_id reply_to_id timestamp 'Thread starter' 'Chat history', prompt cache bust, dist/pi-embedded-D6PpOsxP.js, v2026.3.11" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise and specific about the core bug (Discord thread metadata being re‑injected each turn, fields affected, cache/token impact, and offending compiled file); minor noise is the explicit version tag which isn't needed for deduping." + }, + "usage": { + "input_tokens": 674, + "output_tokens": 721 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "Two runtime regressions in Discord threads: per-turn re-injection of message metadata causing unbounded context growth, and the session resolver falling back to a generic agent:*:main so CLI commands route to the wrong session.", + "solution_summary": "Applied hotfixes to compiled runtime bundles to stop redundant metadata injection and to make resolver lookup use delivery context (channel+to+accountId); added idempotent re-application script and backups, then restarted and validated the gateway.", + "maintainer_signal_summary": "Fixes are manual edits to compiled dist/*.js files (listed in the PR), backed up (*.bak-rootfix-20260313T023340Z) and preserved by an idempotent reapply script (/home/clawd/scripts/openclaw-rootfix-v2026.3.11.sh); these changes will be lost on updates/reinstall so reapplication must be maintained and non-regression tests added.", + "dedupe_summary": "Discord threads; per-turn metadata duplication (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing context growth; session resolver fallback to agent:*:main causing CLI routing to wrong thread-bound session; patched compiled dist/*.js; manual bundle edits with reapply script." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core bugs (per-turn metadata duplication causing context growth and session resolver fallback routing to agent:*:main) with relevant technical terms; minor mention of patched dist files is implementation detail but doesn't harm deduping." + }, + "usage": { + "input_tokens": 650, + "output_tokens": 744 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "OpenClaw fails to send Discord reply messages: outgoing replies error with \"TypeError: fetch failed\" despite Discord proxy being configured and login succeeding; incoming messages and model responses are received/generated correctly.", + "solution_summary": "Ensure the Discord client's REST requests use the configured proxy (e.g., pass a proxy/agent to fetch or honor HTTP(S)_PROXY env vars) so reply POSTs go through the proxy; add handling/fallback when proxy application fails.", + "maintainer_signal_summary": "Regression in 2026.3.2 on Node.js 22/systemd; logs show \"rest proxy enabled\" and \"gateway proxy enabled\" and successful login, but only final reply fetch fails; curl through the same proxy works, indicating the proxy is not being applied to outgoing reply requests in code.", + "dedupe_summary": "discord fetch failed reply proxy-configured rest-proxy gateway-proxy enabled regression openclaw-2026.3.2 node22 systemd" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Captures the core problem (Discord reply send failing with 'fetch failed' despite rest/gateway proxy enabled) using useful technical terms, but includes noisy version/platform metadata that should be removed for cleaner deduping." + }, + "usage": { + "input_tokens": 590, + "output_tokens": 704 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "CLI command 'openclaw agent --channel discord --to ...' routes messages to agent:*:main instead of the live Discord channel/thread session because the session resolver doesn't lookup by delivery context or normalize Discord targets.", + "solution_summary": "Patch the session resolver to normalize Discord targets and perform a lookup by delivery context (channel + to + accountId) before falling back to agent:*:main; change applied to dist/gateway-cli-BjsM6fWb.js as a manual fix.", + "maintainer_signal_summary": "Bug exists in compiled dist file (dist/gateway-cli-BjsM6fWb.js) in v2026.3.11; manual patch has high regression risk and will be overwritten by updates—related issues #36401 and #41562.", + "dedupe_summary": "CLI agent discord session resolver delivery context lookup normalization fallback agent:*:main dist/gateway-cli-BjsM6fWb.js v2026.3.11" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary concisely identifies the core bug (CLI Discord session resolver not normalizing delivery context, causing fallback to agent:*:main) and cites the affected compiled file, but includes an unnecessary version tag which is nonessential for deduping." + }, + "usage": { + "input_tokens": 655, + "output_tokens": 625 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "In v2026.3.11 a regression causes ReferenceError at runtime because dist/compact-1mmJ_KWL.js calls a nonexistent resolveAgentMainSessionKey instead of buildAgentMainSessionKey, breaking the agent command and possibly crashing the gateway.", + "solution_summary": "Manually replace resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...) in dist/compact-1mmJ_KWL.js and add the fix into the source/build so it persists across reinstalls.", + "maintainer_signal_summary": "High-risk manual patch on compiled dist file; this is a regression of fix #3 and must be fixed in the source or build pipeline to avoid losing the patch when reapplying fix #3 or updating OpenClaw.", + "dedupe_summary": "resolveAgentMainSessionKey buildAgentMainSessionKey ReferenceError dist/compact-1mmJ_KWL.js v2026.3.11 regression session-resolver fix#3" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary concisely identifies the exact missing function, the replacement function, the ReferenceError and the affected compiled file, but includes minor noise like the release version and 'fix#3' tag that aren't needed for deduping." + }, + "usage": { + "input_tokens": 626, + "output_tokens": 604 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "Discord channel plugin applies channels.discord.proxy only to WebSocket connections; REST API HTTP requests (used for health checks and message send) ignore the proxy, causing 'fetch failed' errors and timeouts in proxy-required environments.", + "solution_summary": "Make the Discord plugin's HTTP client honor channels.discord.proxy for all REST API calls (health check and message send), e.g., pass proxy config to the HTTP transport or use a shared proxied client; add tests and fix config handling.", + "maintainer_signal_summary": "Regression observed in OpenClaw 2026.3.8: health check reports 'fetch failed' despite UI showing ON|OK; curl with same proxy succeeds, indicating REST calls bypass the configured proxy and break CLI message send.", + "dedupe_summary": "discord REST proxy ignored; channels.discord.proxy; websockets proxied; healthcheck 'fetch failed'; message send timeout; regression 2026.3.8" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Captures the core issue (Discord REST calls not using channels.discord.proxy while websockets are, causing healthcheck 'fetch failed' and message timeouts) but includes an unnecessary regression version/date which is noise." + }, + "usage": { + "input_tokens": 582, + "output_tokens": 688 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the session resolver inside the agent command in dist/compact-1mmJ_KWL.js does not lookup sessions by delivery context (channel+to+accountId), causing sessions to resolve to agent:*:main and responses to be misassociated.", + "solution_summary": "Patched the compiled file dist/compact-1mmJ_KWL.js to perform a delivery-context lookup (channel+to+accountId) in the agent command session resolver and only fallback to main when no specific session exists.", + "maintainer_signal_summary": "Bug exists in a compiled dist file (dist/compact-1mmJ_KWL.js) on v2026.3.11; fix applied manually to dist, so it will be lost on reinstall/update — high regression risk.", + "dedupe_summary": "dist/compact-1mmJ_KWL.js session resolver missing delivery-context lookup (channel+to+accountId) → resolves to agent:*:main; manual compiled-file fix added delivery-context lookup; v2026.3.11, high-regression-risk" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Clear, specific summary of the core bug (missing delivery-context lookup causing resolution to agent:*:main) and the manual fix, but includes unnecessary metadata (version and regression-risk) that aren't needed for deduplication." + }, + "usage": { + "input_tokens": 596, + "output_tokens": 615 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "The Control UI contains most interface text hardcoded in English (labels, buttons, statuses, tooltips), embedded even in built JS bundles, blocking non-English users from effective use.", + "solution_summary": "Extract all hardcoded strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, provide a language selector or auto-detect, and create infrastructure to add more locales later.", + "maintainer_signal_summary": "OpenClaw docs already provide docs/zh-CN; control-ui dist bundles contain hardcoded strings like “Settings”/“Add”/“Delete”; proposer flags Medium-High UX priority and offers to contribute—asks if this is on the roadmap.", + "dedupe_summary": "control-ui hardcoded-English strings; extract i18n; add zh-CN locale" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it identifies the control-ui's hardcoded English strings and the intended i18n extraction plus adding a zh-CN locale, making it ideal for clustering localization requests." + }, + "usage": { + "input_tokens": 517, + "output_tokens": 585 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "WhatsApp channel only supported a root-level systemPrompt, preventing per-account or per-group prompt customization and wildcard fallback behavior for groups.", + "solution_summary": "Extend WhatsApp config/schema to support systemPrompt at account and group levels, implement resolution hierarchy (account overrides root; group resolves specific → wildcard; account groups replace root groups) and compose final injected prompt as account + resolved group separated by a blank line; add tests and docs.", + "maintainer_signal_summary": "Changes touch WhatsApp config types/schema, account resolution, group prompt composition, unit tests and documentation; no changes to non-WhatsApp channels, hot-reload, or group allowlist/mention-gating semantics; closes related issue.", + "dedupe_summary": "whatsapp systemPrompt account-level group-level wildcard groups['*'] account.groups replace root.groups resolution-hierarchy combine account+group config schema tests docs" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Captures the core change (adding WhatsApp systemPrompt at account and group levels, wildcard groups, resolution/merge and config/schema updates) but is terse and includes minor noisy mentions of tests/docs rather than full sentences." + }, + "usage": { + "input_tokens": 2293, + "output_tokens": 502 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages sent via messages.upsert(type=\"append\") were dropped before agent inbound processing, and WhatsApp group policy resolution could ignore account-scoped settings (groupAllowFrom/allowFrom).", + "solution_summary": "Enable append processing only for WhatsApp group messages (DM append still skipped) and propagate accountId so account-scoped groupAllowFrom/allowFrom are used when resolving WhatsApp group policy defaults.", + "maintainer_signal_summary": "Changes are scoped to WhatsApp inbound and group-activation code (monitor.ts and group-activation helpers): enable group append handling, propagate accountId to policy resolution; no schema/dist/debug changes and DM append remains skipped; tests and live repro validated.", + "dedupe_summary": "whatsapp group append dropped messages.upsert inbound; policy resolved without accountId ignoring account-scoped groupAllowFrom/allowFrom allowlist gating" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise summary captures the two core issues (WhatsApp group 'append' messages being dropped and policy resolution ignoring account-scoped allowlist gating), though the phrasing is slightly terse and could be clearer." + }, + "usage": { + "input_tokens": 1845, + "output_tokens": 637 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because resolveClientIp returned undefined for trusted-proxy requests lacking X-Forwarded headers and isLocalDirectRequest did not fall back to the socket address when allowRealIpFallback was enabled.", + "solution_summary": "Make resolveClientIp return req.socket.remoteAddress when no forwarding headers are present; update isLocalDirectRequest to check req.socket.remoteAddress when allowRealIpFallback=true; gate .ts.net host acceptance behind auth.allowTailscale and thread that flag through callers; update docs and tests.", + "maintainer_signal_summary": "Adds unit tests (src/gateway/auth.test.ts, src/gateway/net.test.ts), modifies gateway auth/net/server files, preserves default allowTailscale=true when tailscale.mode='serve', closes #28472 and depends on PR #50636; verified end-to-end and documents required trustedProxies and allowRealIpFallback settings.", + "dedupe_summary": "Tailscale Serve Canvas 401; resolveClientIp lacked socket-address fallback; isLocalDirectRequest missing req.socket.remoteAddress fallback; .ts.net host acceptance gated by allowTailscale." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary of the root causes and fixes for Canvas 401 behind Tailscale Serve that omits boilerplate and would match similar proxy/auth fallback issues." + }, + "usage": { + "input_tokens": 1684, + "output_tokens": 763 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "checkInboundAccessControl() computed isSelfChat but never used, so WhatsApp self-chat deployments with account.selfChatMode true allowed non-self messages (groups, stranger DMs, broadcasts) because groupPolicy defaulted to \"open\", violating self-chat isolation.", + "solution_summary": "Add an early-return guard in checkInboundAccessControl() that, when account.selfChatMode === true, blocks all non-self messages (returns allowed:false) before any group/DM policy evaluation, while allowing owner messages via isSamePhone and keeping isSelfChat computed for UI/logging.", + "maintainer_signal_summary": "Change limited to src/web/inbound/access-control.ts; guard placed before policy resolution; auto-detected heuristic (isSelfChatMode()) unchanged by design; all related access-control tests pass; revert by removing the early-return guard.", + "dedupe_summary": "WhatsApp self-chat not enforced; checkInboundAccessControl isSelfChat unused; non-self messages allowed due to groupPolicy default open; fix: early-return guard on account.selfChatMode true blocks non-self (uses isSamePhone to allow owner); file src/web/inbound/access-control.ts; tests pass." + }, + "judge": { + "score": 4, + "has_boilerplate": true, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the bug, root cause, and fix (mentions checkInboundAccessControl, groupPolicy default, and the guard), but includes minor noise like a file path and 'tests pass'." + }, + "usage": { + "input_tokens": 1399, + "output_tokens": 721 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "With groupPolicy: \"allowlist\", group messages from senders not in groupAllowFrom are dropped at the access-control layer and never stored as context, so the agent misses those messages in its contextual history despite docs expecting them to be stored for context-only.", + "solution_summary": "Add an opt-in groupContextFromAll flag (top-level or per-account, default false) that makes access-control return storeForContext for non-allowlisted group senders; those messages are marked contextOnly, recorded in group history, and do not trigger replies or send read receipts.", + "maintainer_signal_summary": "Modifies access-control.ts (adds storeForContext), inbound monitor (contextOnly handling, skip read receipts), types, auto-reply on-message (recordPendingHistoryEntryIfEnabled), config types and Zod schema, and accounts resolution; default false preserves backward compatibility; watch allowlist normalization and potential gating/state-update bypasses as merge risks.", + "dedupe_summary": "WhatsApp groupPolicy allowlist drops non-allowlisted messages; add groupContextFromAll flag to storeForContext→contextOnly via access-control+monitor; store pending context, no-reply, no-read-receipts, groupAllowFrom, mention-gating" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summarizes the core bug (allowlist groupPolicy drops non-allowlisted messages) and the proposed fix (add groupContextFromAll to store messages as pending context via access-control/monitor), but includes a few minor extra terms (no-reply, no-read-receipts) that are slightly noisy." + }, + "usage": { + "input_tokens": 1416, + "output_tokens": 839 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "Add an optional sessionKey to sessions_spawn to let sub-agents reuse deterministic sessions instead of new UUIDs; short keys are namespaced to agent:{agentId}:subagent:{key}, fully-qualified keys are validated against targetAgentId, otherwise fallback to crypto.randomUUID().", + "solution_summary": "Implement sessionKey handling (short-key namespacing, fully-qualified key passthrough with agentId validation), add tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch, and patch allowlist bypass.", + "maintainer_signal_summary": "Tests currently fail to compile due to wrong import (openclaw-tools.js vs clawdbot-tools.js); validation has a case-sensitivity bug (embedded agentId not normalized) causing false rejections; inconsistent error handling uses throw in one place vs jsonResult elsewhere—fix tests, normalize IDs, and unify error responses.", + "dedupe_summary": "sessions_spawn sessionKey reuse deterministic session short-key namespace agent:{agentId}:subagent:{key} fully-qualified key validation cross-agent mismatch case-sensitivity normalizeAgentId inconsistent error handling throw jsonResult broken test import openclaw-tools.js clawdbot-tools.js allowlist bypass tests" + }, + "judge": { + "score": 3, + "has_boilerplate": false, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Contains the core change (sessions_spawn sessionKey for deterministic sub-agent sessions, short-key namespace mapping, fully-qualified validation, and allowlist bypass fix) but includes noisy test/import/error-handling details and implementation minutiae that reduce signal clarity." + }, + "usage": { + "input_tokens": 1469, + "output_tokens": 913 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Editing Telegram messages with media (photo, video, audio, document, animation) used editMessageText and caused Telegram 400 error “there is no text in the message to edit”, so caption edits silently failed and left UI inconsistent.", + "solution_summary": "editMessageTelegram now catches that specific 400 (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption for captionable media; buttons-only updates use editMessageReplyMarkup; MESSAGE_NOT_MODIFIED is treated as success and interface is unchanged.", + "maintainer_signal_summary": "Change limited to src/telegram/send.ts, 6 new unit tests cover retry, buttons-only, error propagation and MESSAGE_NOT_MODIFIED; backward compatible with no API surface changes; risk noted that regex may break if Telegram error text changes.", + "dedupe_summary": "telegram editMessageText 400 \"no text in the message to edit\" -> retry editMessageCaption; captionable media photo video audio document animation; editMessageReplyMarkup for buttons-only; NO_TEXT_IN_MESSAGE_RE; src/telegram/send.ts" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core fix: catch Telegram's 'no text in the message to edit' 400, retry with editMessageCaption for captionable media, use editMessageReplyMarkup for buttons-only, and points to the implementation location." + }, + "usage": { + "input_tokens": 1350, + "output_tokens": 510 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "PR #24962 hardcoded disableBlockStreaming: true for WhatsApp, causing all messages to be batched and preventing config-driven block streaming (e.g., blockStreamingDefault:on, blockStreamingBreak:text_end).", + "solution_summary": "Modify deliver callback to suppress only tool payloads and drive disableBlockStreaming from per-account channels.whatsapp.blockStreaming with fallback to agents.defaults.blockStreamingDefault, restoring progressive block streaming while keeping reasoning suppressed upstream.", + "maintainer_signal_summary": "Live WhatsApp verification and unit tests pass; reasoning/tool suppression remains enforced upstream; change is backward-compatible and reversible via channels.whatsapp.blockStreaming:false.", + "dedupe_summary": "whatsapp hardcoded disableBlockStreaming PR24962 blocked blockStreaming config channels.whatsapp.blockStreaming agents.defaults.blockStreamingDefault deliver callback suppressed everything fixed to suppress only tool payloads shouldSuppressReasoningPayload upstream" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary captures the core change: WhatsApp's disableBlockStreaming was hardcoded and blocked config-driven block streaming, now restored to use per-account config and the deliver callback only suppresses tool payloads because reasoning is filtered upstream." + }, + "usage": { + "input_tokens": 1342, + "output_tokens": 627 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Sending videos >5MB caused the Telegram gateway to hang because getFile/content-length checks left the HTTP response body unconsumed, causing undici Agent.close to deadlock in closeDispatcher.", + "solution_summary": "Increased default channels.telegram.mediaMaxMb from 5 to 20; fixed closeDispatcher by adding a timeout and destroy fallback to avoid Agent.close deadlock; added timeouts/retries to getFile and download logic; made media download failures non-fatal; stripped inbound media path metadata from UI.", + "maintainer_signal_summary": "Changes touch src/infra/net/ssrf.ts and src/telegram/bot.ts; default mediaMaxMb changed (configurable); tests updated (strip-inbound-meta.test.ts passing); no SSRF policy or new-dependency changes. To revert quickly, reset channels.telegram.mediaMaxMb to 5 or restore the modified files.", + "dedupe_summary": "telegram gateway hang video unconsumed response body undici Agent.close deadlock closeDispatcher timeout destroy fallback mediaMaxMb 5→20 getFile content-length exceeds maxBytes retries timeouts non-fatal media errors" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that highlights the root cause (undici Agent.close deadlock from unconsumed response body) and key fixes (increase mediaMaxMb, timeouts/retries, non-fatal media errors), so it will cluster with similar Telegram media/gateway hang issues." + }, + "usage": { + "input_tokens": 1438, + "output_tokens": 881 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "Telegram document attachments with no caption were misidentified because resolveTelegramInboundBody hardcoded the fallback body text as , causing agents to treat documents as images (e.g., PDFs reported as not received).", + "solution_summary": "Use the precomputed type-aware placeholder in resolveTelegramInboundBody so the fallback body emits , , etc., and add unit tests validating media placeholders and document handling.", + "maintainer_signal_summary": "Bug fix in extensions/telegram; adds unit tests (helpers.test.ts, bot.create-telegram-bot.test.ts); closes #7116; no changes to media download or file-extraction logic; backward compatible with narrow edge-case risk.", + "dedupe_summary": "telegram document placeholder mislabel hardcoded instead of type-aware in resolveTelegramInboundBody fallback" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the bug and code location (resolveTelegramInboundBody) describing the wrong hardcoded placeholder and correct type-aware , with no boilerplate noise." + }, + "usage": { + "input_tokens": 1265, + "output_tokens": 590 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "MEDIA_MAX_BYTES (5 MB) was hardcoded in src/media/store.ts, causing files larger than 5 MB to silently fail staging into sandboxed containers.", + "solution_summary": "Introduce optional media.maxBytes config (default 5 MB, max 500 MB), replace hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(), read setting at gateway boot, update error messages and tests accordingly.", + "maintainer_signal_summary": "Closes #40880; unit tests pass; addressed bot review comments; backward compatible (default preserved, deprecated constant still exported); schema enforces 500 MB cap; changes touch gateway boot and media store code.", + "dedupe_summary": "media.maxBytes config replaces hardcoded MEDIA_MAX_BYTES (5MB); getMediaMaxBytes/setMediaMaxBytes used at gateway boot; schema max 500MB; store.ts and error messages updated." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, noise-free summary that captures the core change—making MEDIA_MAX_BYTES configurable via media.maxBytes with getter/setter usage at gateway boot and related schema/store/error updates—so it will cluster with similar config/limit changes." + }, + "usage": { + "input_tokens": 1386, + "output_tokens": 797 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "When Telegram media/file download fails (due to size limit or API error) the handler returned early and the message was silently dropped, so the agent received no ack and users got no feedback.", + "solution_summary": "On download failure extract file metadata (name, size, type) from the Telegram message, inject a metadata stub into msg.text, and allow the message to continue through processMessage instead of returning early.", + "maintainer_signal_summary": "Integration change in src/telegram/bot-handlers.ts: replace early returns on download errors with buildFileMetadataStub and msg.text mutation; normal media download path, media groups, stickers, and outbound sending unchanged and backward compatible.", + "dedupe_summary": "Telegram media download failure silent drop; inject file metadata stub into msg.text and continue pipeline; fixes >mediaMaxMb (default 5MB) and Telegram getFile 20MB limit; bot-handlers.ts return-on-error removed." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core change—prevents silent drops when Telegram media download fails by injecting file metadata into msg.text and continuing the pipeline (includes relevant size-limit context and removed early return)." + }, + "usage": { + "input_tokens": 1228, + "output_tokens": 665 + } + }, + { + "thread_id": 5796, + "number": 24117, + "kind": "pull_request", + "title": "feat(cli): add --session-key targeting for openclaw agent", + "summary": { + "problem_summary": "openclaw agent has no --session-key option, so CLI-triggered deliveries often rely on unstable UUID sessionId or sender-derived --to and can route to the wrong (agent main) session in automation/callback flows.", + "solution_summary": "Add a --session-key CLI flag to openclaw agent, add sessionKey to AgentCliOpts and forward it into resolveSessionKeyForRequest/gateway, update help/validation, and add tests; session-key takes precedence.", + "maintainer_signal_summary": "Code changes in src/cli/program/register.agent.ts and src/commands/agent-via-gateway.ts, tests added in register.agent.test.ts and agent-via-gateway.test.ts; local tests pass; change is additive and backward-compatible with no core resolver semantics altered.", + "dedupe_summary": "openclaw agent --session-key CLI flag stable session targeting forwarded via AgentCliOpts->resolveSessionKeyForRequest->gateway precedence over --session-id/--to/--agent tests" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that names the new --session-key flag, its purpose (stable session targeting), implementation path (AgentCliOpts->resolveSessionKeyForRequest->gateway) and precedence over other flags, making it ideal for clustering similar changes." + }, + "usage": { + "input_tokens": 1200, + "output_tokens": 864 + } + }, + { + "thread_id": 766, + "number": 11611, + "kind": "pull_request", + "title": "feat: separate group-level allowlist from sender-level command authorization", + "summary": { + "problem_summary": "With groupPolicy set to \"allowlist\", the system currently gates all group interactions by sender phone number via groupAllowFrom, preventing group-level JID allowlisting, owner-only command restriction, and blocking spam groups while allowing open chat in approved groups.", + "solution_summary": "When groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access is gated by group JIDs from that mapping (via resolveChannelGroupPolicy), allowing any participant in approved groups to chat while retaining groupAllowFrom for slash-command authorization; falls back to sender-based filtering when no groups mapping is present.", + "maintainer_signal_summary": "Code change in src/web/inbound/access-control.ts to use resolveChannelGroupPolicy, docs update in src/config/types.whatsapp.ts, new tests in access-control.group-allowlist.test.ts; depends on #3326 and preserves legacy sender-based behavior when groups config absent.", + "dedupe_summary": "WhatsApp groupPolicy:allowlist uses channels.whatsapp.groups (group JID allowlist) for inbound gating; groupAllowFrom still restricts slash commands; fallback to sender-based filtering when no groups mapping; key funcs: checkInboundAccessControl, resolveChannelGroupPolicy; prevents spam groups while allowing open chat in approved groups." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change (use group JID allowlist for inbound gating, preserve sender-based command auth, fallback behavior) without boilerplate and would match similar issues." + }, + "usage": { + "input_tokens": 1250, + "output_tokens": 622 + } + }, + { + "thread_id": 17924, + "number": 46425, + "kind": "pull_request", + "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", + "summary": { + "problem_summary": "Telegram account-configured mediaMaxMb was ignored for outbound reply messages because loadWebMedia() in the reply delivery path received no maxBytes and fell back to the 100 MB default.", + "solution_summary": "Thread mediaMaxBytes through the Telegram outbound reply delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia() receives configured maxBytes; add unit tests.", + "maintainer_signal_summary": "Small, backwards-compatible bugfix limited to Telegram integration; touches five delivery-path files, adds two unit tests, all related test suites pass; closes #46023.", + "dedupe_summary": "telegram mediaMaxMb ignored outbound replies; loadWebMedia maxBytes missing; threaded mediaMaxBytes through bot.ts, bot-message.ts, bot-message-dispatch.ts, delivery.replies.ts, bot-native-commands.ts; unit tests added." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise and specific about the bug (mediaMaxMb ignored for Telegram outbound replies) and the code paths/files changed, but includes a minor testing detail ('unit tests added') that isn't needed for deduping." + }, + "usage": { + "input_tokens": 1251, + "output_tokens": 438 + } + }, + { + "thread_id": 5712, + "number": 37050, + "kind": "pull_request", + "title": "fix(telegram): add HTML parse fallback for media captions", + "summary": { + "problem_summary": "Telegram media sends (photo/document/video/animation/audio/voice) lacked an HTML-parse fallback, so captions containing invalid HTML/entities caused Telegram to return a 400 and the media send to fail.", + "solution_summary": "Wrap the media send callback with withTelegramHtmlParseFallback so on an HTML parse error the send is retried without parse_mode using the raw caption; applied to all media types.", + "maintainer_signal_summary": "A unit test was added to assert first call uses parse_mode:'HTML' and the retry omits parse_mode; change is limited to the media send inner callback (text/edit/thread/caption-splitting logic unchanged) and can be reverted by restoring src/telegram/send.ts.", + "dedupe_summary": "telegram media caption HTML-parse-error → 400; add withTelegramHtmlParseFallback retry without parse_mode for sendPhoto/sendDocument/sendVideo/sendAnimation/sendAudio/sendVoice" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the HTML-parse-error causing 400 for media captions and the added withTelegramHtmlParseFallback retry (no parse_mode) across the listed send* media methods, which is ideal for deduplication." + }, + "usage": { + "input_tokens": 1125, + "output_tokens": 723 + } + }, + { + "thread_id": 21769, + "number": 55280, + "kind": "pull_request", + "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", + "summary": { + "problem_summary": "WhatsApp had block streaming effectively disabled (hardcoded/incorrectly wired) and a race in the block-reply delivery pipeline caused intermediate blocks to be lost or final replies suppressed, producing silent turns.", + "solution_summary": "Make blockStreaming configurable per-account/global, add sensible coalescing defaults, introduce dispatcher.sendBlockReplyAsync() to await confirmed delivery before marking sentContentKeys, and fix the disableBlockStreaming undefined bug; tests updated.", + "maintainer_signal_summary": "Updates require dispatcher API support for sendBlockReplyAsync in mocks/tests, config key channels.whatsapp.blockStreaming (default false) now respected, and the change fixes the root cause of silent turns rather than the prior workaround; existing tests modified/added.", + "dedupe_summary": "whatsapp blockStreaming configurable delivery race sendBlockReplyAsync sentContentKeys disableBlockStreaming undefined bug block-reply pipeline" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Contains the core technical signals (WhatsApp blockStreaming configurability and a delivery-race bug in the block-reply pipeline) but is terse and keywordy rather than a short, clear 1–3 sentence summary." + }, + "usage": { + "input_tokens": 1038, + "output_tokens": 513 + } + }, + { + "thread_id": 8098, + "number": 25543, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked support for per-group allowlist entries, so specific Signal groups could not be explicitly allowed to bypass sender-level groupAllowFrom checks or run control commands.", + "solution_summary": "Introduce channels.signal.groups. via a new SignalGroupConfig and Zod schema, wire resolveChannelGroupPolicy into the Signal event handler and commandGate so listed groups bypass groupAllowFrom and are authorized for commands, and update docs.", + "maintainer_signal_summary": "Implementation follows existing channel patterns (Telegram/iMessage/etc.), TypeScript and Zod validations pass, addresses prior review feedback, and is low-risk and safe to merge.", + "dedupe_summary": "signal channels.signal.groups group-level allowlist groupAllowFrom bypass resolveChannelGroupPolicy commandGate SignalGroupConfig Zod-schema" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise, technical keywords capture the core change (adding a group-level allowlist/config for Signal and related code/schema hooks) but is terse and could be a one-sentence description rather than a keyword list." + }, + "usage": { + "input_tokens": 980, + "output_tokens": 583 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel treated groupAllowFrom entries as sender identifiers, so group IDs were not recognized and group messages were silently dropped when groupPolicy was set to \"allowlist\".", + "solution_summary": "Add a groups config to SignalAccountSchemaBase and call resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom; include 4 tests to verify behavior.", + "maintainer_signal_summary": "Small change (size S) targeting channel: signal; fixes #25540; adds schema entry, event-handler patch, and tests; all relevant tests pass.", + "dedupe_summary": "signal groups-config missing resolveChannelGroupPolicy group-level-allowlist failure groupAllowFrom-as-sender silent-drop" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies Signal lacked a group-level allowlist (missing groups config/resolveChannelGroupPolicy) causing groupAllowFrom to be misapplied as a sender filter and drop group messages, which is sufficient to cluster similar issues." + }, + "usage": { + "input_tokens": 780, + "output_tokens": 707 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "WhatsApp channel currently hardcodes disableBlockStreaming: true in process-message.ts, causing intermediate agent message blocks to be accumulated and delivered as a single final WhatsApp message instead of separate block-streamed messages.", + "solution_summary": "Make WhatsApp respect channels.whatsapp.blockStreaming by adding resolveWhatsAppBlockStreamingEnabled(), replacing the hardcoded disable flag, treating intermediate blocks as valid replies when enabled, registering blockStreaming and DEFAULT_BLOCK_STREAMING_COALESCE in the WhatsApp dock, and updating tests.", + "maintainer_signal_summary": "Changes touch process-message.ts, dock.ts, and process-message.inbound-contract.test.ts; tests updated and all existing tests pass; small change (size: S); behavior remains backwards-compatible when blockStreaming is false.", + "dedupe_summary": "WhatsApp disableBlockStreaming hardcoded; add channels.whatsapp.blockStreaming; resolveWhatsAppBlockStreamingEnabled; intermediate-block delivery vs single-batch; dock register DEFAULT_BLOCK_STREAMING_COALESCE; process-message.ts" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core change (remove hardcoded disableBlockStreaming for WhatsApp, add channels.whatsapp.blockStreaming, implement resolveWhatsAppBlockStreamingEnabled, and allow intermediate block deliveries) and references the relevant files, so it will cluster with similar PRs/issues." + }, + "usage": { + "input_tokens": 530, + "output_tokens": 767 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v5-component-focused.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v5-component-focused.json new file mode 100644 index 0000000..7eed1cb --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v5-component-focused.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v5-component-focused", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "Summarize this GitHub issue or pull request for automated duplicate detection. Your summary will be embedded and clustered.\n\nStructure your analysis:\n1. First identify the COMPONENT or SUBSYSTEM (e.g., \"Discord gateway\", \"WhatsApp delivery\", \"Telegram media handler\", \"CLI routing\", \"session management\")\n2. Then identify the SPECIFIC PROBLEM or CHANGE within that component\n3. Combine into a clear dedupe_summary that starts with the component name\n\nIgnore completely: template boilerplate, testing instructions, checklists, environment info, reproduction steps, deployment notes, version numbers, cross-references.\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nPlain text, no markdown, 1-3 sentences each.\ndedupe_summary format: \"[Component]: [specific issue or change]\" — this helps cluster by subsystem.", + "timestamp": "2026-03-30T17:20:46.303Z", + "aggregate": { + "avg_score": 4.97, + "avg_signal_density": 4.97, + "boilerplate_count": 0, + "boilerplate_pct": 0, + "would_cluster_correctly_pct": 100, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 45423, + "total_output_tokens": 24588 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "Discord channel proxying (channels.discord.proxy) only applies to the WebSocket Gateway; REST API calls (command deploy, fetch bot identity) and guild resolution are not routed through the configured HTTP proxy, causing fetch failures, unresolved guild channels, and no guild message handling when the host requires an HTTP proxy.", + "solution_summary": "Apply the configured proxy to the undici dispatcher used for all Discord REST calls and the guild-resolve code path (not just the Gateway WS). Consider a global proxy config that sets a ProxyAgent for undici, and add retries/error handling for guild resolution.", + "maintainer_signal_summary": "Startup logs show WS connects via proxy but REST calls time out with \"fetch failed\"; setting a global undici ProxyAgent fixes REST but not guild resolve; CLI guild resolve succeeds when bootstrapped with ProxyAgent; issue is a regression impacting users behind HTTP proxies and reproducible on local macOS with a proxy tool.", + "dedupe_summary": "Discord channel (channels.discord.proxy): REST API and guild resolution are not proxied—only the WebSocket is—breaking guild channels behind an HTTP proxy." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states the exact subsystem (channels.discord.proxy), the technical failure (REST API/guild resolution not proxied while WebSocket is), and the user impact, so it will cluster correctly with similar proxy-related Discord channel issues." + }, + "usage": { + "input_tokens": 2271, + "output_tokens": 563 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "Component: Discord channels/RequestClient. Outbound REST API calls for sending messages bypass the configured channels.discord.proxy, causing fetch to fail in proxied/restricted networks (user reports TypeError: fetch failed while WebSocket gateway receiving works). The Carbon RequestClient’s fetch implementation appears to have no proxy support, leading to message-send failures in regions that require a proxy.", + "solution_summary": "Ensure REST/send requests honor channels.discord.proxy by adding proxy support to the RequestClient/fetch layer (support HTTP/SOCKS/TUN as applicable) and route attachment downloads/uploads through the configured proxy. Update RequestClient to accept and apply proxy configuration or use a proxy-capable HTTP client so REST operations use the same proxy as the gateway.", + "maintainer_signal_summary": "User repro in a China/proxy-required environment with logs noting rest proxy enabled but REST not using proxy; gateway (WebSocket) works while REST fails, blocking message sends. Labeled bug and high impact for users in restricted networks—prevents normal bot replies and media operations.", + "dedupe_summary": "Discord channels (REST/RequestClient): channels.discord.proxy not applied to outbound REST sends; RequestClient.fetch lacks proxy support" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific description that the channels.discord.proxy config isn't used for REST requests because RequestClient.fetch has no proxy support, which will match similar bug reports." + }, + "usage": { + "input_tokens": 953, + "output_tokens": 675 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "Discord provider (OpenClaw) behind an HTTP proxy partially starts: gateway/WebSocket login succeeds and application ID fetches, but some REST startup calls consistently fail with “fetch failed” (notably native command deployment and bot identity fetch), making the provider appear half-working.", + "solution_summary": "Ensure all Discord REST request paths use the same proxy-aware fetch/dispatcher as the gateway/login path (e.g., centralize HTTP client or attach proxy agent to global fetch), add tests for proxy coverage across startup code paths, and fix any code paths bypassing configured proxy.", + "maintainer_signal_summary": "Logs show rest proxy and gateway proxy enabled, applicationId fetch succeeds, but deploy-rest and fetch-bot-identity fail repeatedly with \"fetch failed\"; issue reproducible by user and likely high-impact because provider appears logged in but REST features are broken. User can provide environment details or test patches.", + "dedupe_summary": "Discord provider: REST startup calls (native commands deployment and bot identity fetch) fail with 'fetch failed' when HTTP proxy is configured, while gateway/login succeeds." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core failure (Discord REST startup calls for command deployment and bot identity fetch failing with a proxy while gateway/login works), so it will match similar reports." + }, + "usage": { + "input_tokens": 1099, + "output_tokens": 523 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "Discord channel proxy is logged as enabled but the REST API and gateway connections fail with \"fetch failed\" when OpenClaw runs under WSL2 using an HTTP proxy. The proxy setting (~/.openclaw/openclaw.json channels.discord.proxy) appears recognized in logs but is not applied to the underlying Discord.js/fetch client or WebSocket, leaving gatewayConnected=false.", + "solution_summary": "Ensure the configured HTTP proxy is actually passed to Discord.js/undici/fetch and the WebSocket client (e.g., instantiate and attach an HTTP/HTTPS/WS proxy agent or set the appropriate undici/global agent), so REST calls and gateway connections use the proxy under WSL2. Add tests and logging to verify fetch/gateway use the proxy and handle proxy-specific edge cases in WSL2.", + "maintainer_signal_summary": "Reporter marks severity as high: Discord channel completely non-functional for users behind an HTTP proxy (100% reproducible in WSL2). Logs show proxy enabled messages but repeated fetch failures; workaround is using a different channel or TUN-mode transparent proxy; config and curl verification included.", + "dedupe_summary": "Discord channel: proxy logged as enabled but REST/gateway requests fail with \"fetch failed\" because the Discord client is not applying the configured HTTP proxy (WSL2)" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states that although the Discord proxy is logged as enabled, the client isn't applying the configured HTTP proxy (WSL2) causing REST/gateway 'fetch failed' errors, which is ideal for deduplication." + }, + "usage": { + "input_tokens": 936, + "output_tokens": 728 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "Discord channel REST API calls ignore configured proxy (channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY env vars) and fail with \"TypeError: fetch failed\" when sending messages, while the WebSocket connection and other channels (e.g., Telegram) work through the proxy and curl via proxy succeeds.", + "solution_summary": "Route Discord REST API requests through the same proxy-aware fetch/SSRF guard path used for web tools (web_search/web_fetch) so the gateway honors channels.discord.proxy and standard HTTP_PROXY/HTTPS_PROXY settings for outbound REST calls.", + "maintainer_signal_summary": "Logs show Discord final reply failed with fetch error; changelog notes a recent proxy-aware fix for web tools but explicitly excludes Discord REST API; user provided reproducible steps and curl proof that proxy itself works, and offered a suggested fix and a limited workaround (webhooks).", + "dedupe_summary": "Discord REST API: REST calls ignore configured proxy (channels.discord.proxy and env vars) causing fetch failures when sending messages; WebSocket and other channels use proxy correctly." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug — Discord REST calls ignore configured proxy (both config and env vars) causing fetch failures while WebSocket still uses the proxy — with no boilerplate or irrelevant details." + }, + "usage": { + "input_tokens": 959, + "output_tokens": 497 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "CLI session resolver for the agent command incorrectly routes Discord deliveries to the generic agent:*:main session instead of the live channel/thread session because it does not lookup sessions by real delivery context (channel + to + accountId), fails to normalize Discord targets, and the command path did not propagate necessary context.", + "solution_summary": "Applied manual patches to compiled dist files to add delivery-target-based session resolution, normalize Discord targets (e.g., prefix channel IDs), add lookup by delivery context in the agent resolver, fix use of buildAgentMainSessionKey, and pass channel and replyAccount/accountId through the command path; validated CLI now routes to correct channel sessions.", + "maintainer_signal_summary": "High-risk manual edits were made directly in dist/ compiled files and will be lost on updates; maintainers should port fixes into source, add tests for delivery-context session resolution, and include a post-update validation step to prevent regressions.", + "dedupe_summary": "CLI session resolver: 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel/thread session due to missing delivery-context lookup and Discord target normalization" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that states the exact CLI command misrouting and the specific root causes (missing delivery-context lookup and Discord target normalization), so it will reliably match duplicates." + }, + "usage": { + "input_tokens": 886, + "output_tokens": 644 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions in Discord session handling: (1) volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) was being re-injected on every turn, causing progressive context bloat and degraded thread performance; (2) CLI agent commands routed to the wrong session (defaulting to agent:*:main) instead of the live thread-bound session.", + "solution_summary": "Patched runtime bundles to stop reinjecting volatile Discord thread metadata; updated CLI/gateway code to resolve sessions by actual delivery context (normalizing Discord targets and using channel + to + accountId), passed delivery context into the session resolver, and fixed a regression by switching back to buildAgentMainSessionKey where appropriate.", + "maintainer_signal_summary": "Modified compiled/runtime bundle files in openclaw/dist (pi-embedded, gateway-cli, compact, register.agent); manual patches were applied to compiled bundles and will be lost on update—maintainers should reapply or provide a persistent fix; verification shows threads no longer carry repeated context and CLI routes to the correct session.", + "dedupe_summary": "Discord gateway/thread session management: prevent reinjection of volatile Discord thread metadata each turn and fix CLI routing to resolve sessions by actual delivery target (channel + to + accountId)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the two core issues—repeated injection of volatile Discord thread metadata and incorrect CLI session resolution by delivery context—without boilerplate, so it will cluster correctly." + }, + "usage": { + "input_tokens": 805, + "output_tokens": 680 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "Component: tools/sandbox (sandbox media staging). The MEDIA_MAX_BYTES constant is hardcoded to 5 MB in built files (dist/store-*.js), causing files larger than 5 MB to be rejected or silently fail when copied into sandboxed agents.", + "solution_summary": "Expose the media staging cap as a user-configurable setting (e.g., tools.media.maxBytes in openclaw.json) and have the sandbox loader read that config instead of using the hardcoded 5 MB constant; ensure build artifacts reflect the configurable value.", + "maintainer_signal_summary": "Reporter marks this a regression (worked before), present since v2026.3.7 and still in v2026.3.8, with a manual sed patch workaround applied after npm install -g; labels: bug, regression; no explicit severity provided.", + "dedupe_summary": "tools/sandbox: MEDIA_MAX_BYTES is hardcoded to 5 MB in dist/store-*.js and should be made configurable via openclaw.json (tools.media.maxBytes)" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the hardcoded 5 MB limit in dist/store-*.js and the exact desired config key (tools.media.maxBytes), which is ideal for deduplication." + }, + "usage": { + "input_tokens": 852, + "output_tokens": 546 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "Discord REST API requests fail with TypeError 'fetch failed' when the gateway tries to deploy commands or send messages, even though proxy is configured in openclaw.json and logs show 'rest proxy enabled'. The WebSocket gateway connection works and Telegram with same proxy works; direct curl calls through the proxy succeed.", + "solution_summary": "Ensure the Discord REST client uses the configured proxy for HTTP(S) requests (e.g., attach a proxy agent to Node's fetch/undici or honor HTTP(S)_PROXY), so REST calls route through the proxy like curl and stop producing 'fetch failed'.", + "maintainer_signal_summary": "Issue includes gateway logs showing 'rest proxy enabled' and repeated 'fetch failed' errors; bot receives messages but cannot send; curl with same proxy succeeds; environment: macOS, Node.js 25.6.1, OpenClaw build noted; labeled as a bug.", + "dedupe_summary": "Discord REST API: REST requests fail with 'fetch failed' despite proxy being configured and shown enabled, while gateway WebSocket and curl+proxy work correctly." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific core problem statement: REST API calls error with 'fetch failed' despite proxy configured and enabled, while gateway and curl+proxy succeed, which is exactly the signal needed to cluster similar reports." + }, + "usage": { + "input_tokens": 797, + "output_tokens": 740 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "Discord REST API requests made via @buape/carbon's RequestClient (and other fetch-based code paths like memorySearch and attachment downloads) ignore the channels.discord.proxy setting in openclaw.json because RequestClient calls globalThis.fetch without a proxy-aware dispatcher; Node.js 22 native fetch also doesn't honor HTTP_PROXY/HTTPS_PROXY without --use-env-proxy.", + "solution_summary": "Change @buape/carbon's REST client to accept and use an explicit dispatcher or proxy option, and have OpenClaw pass channels.discord.proxy into the client so REST calls use the same proxy as the WebSocket gateway; alternatively document usage of an undici EnvHttpProxyAgent preload as a temporary workaround (avoid NODE_OPTIONS=--use-env-proxy).", + "maintainer_signal_summary": "This is a cross-cutting proxy handling bug affecting multiple fetch-based code paths (REST requests, embeddings, attachment downloads) due to Node 22 fetch behavior; a library-level change in @buape/carbon plus a configuration pass-through from OpenClaw is the preferred fix; workaround exists via setGlobalDispatcher with undici.", + "dedupe_summary": "Discord REST client (@buape/carbon RequestClient): ignores channels.discord.proxy and calls globalThis.fetch without a proxy-aware dispatcher" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: names the component (@buape/carbon RequestClient), the config key (channels.discord.proxy), and the observed behavior (uses globalThis.fetch without a proxy-aware dispatcher), so it cleanly captures the core issue for deduplication." + }, + "usage": { + "input_tokens": 806, + "output_tokens": 516 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "OpenClaw suffers performance degradation under sustained load with long-running conversations: unbounded context token growth (writer ~42k tokens, reasoning ~90k), Discord lane blocking causing serialization under load, and typing indicators that can remain stuck until TTL.", + "solution_summary": "Applied configuration limits and timeouts: reduced max concurrent sessions/agents/subagents and children, set agents.defaults.contextTokens to 80000, increased channels.discord.eventQueue.listenerTimeout to 300000, restarted the gateway, and added an anti-regression patching script to cover reply/compact/pi/gateway-cli/register variants.", + "maintainer_signal_summary": "Gateway restarted and running; no new 'lane wait exceeded' or 'typing TTL reached' errors observed post-restart; existing dist patches intact; restart mitigates but does not fully eliminate issues under sustained load; recommendation to expose adaptive or tunable limits via openclaw config.", + "dedupe_summary": "session management: unbounded context token growth and lane blocking under load — enforced context token limits, reduced concurrency settings, and increased Discord event queue listener timeout." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core issue (unbounded context token growth and lane blocking under load) and the specific mitigations (context limits, reduced concurrency, increased Discord event queue timeout) with no boilerplate, so it will cluster correctly." + }, + "usage": { + "input_tokens": 740, + "output_tokens": 739 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every conversation turn, contaminating context, increasing token usage, and breaking prompt caching because the runtime lacks a guard to limit injection to the first turn.", + "solution_summary": "A manual patch was applied to compiled dist files: in dist/pi-embedded-D6PpOsxP.js metadata injection for Discord threads was prevented, and in dist/compact-1mmJ_KWL.js an isNewSession guard was added and resolveAgentMainSessionKey was replaced with buildAgentMainSessionKey(...). Validation shows sessions and prompt caching work again, and thread context no longer repeats.", + "maintainer_signal_summary": "High regression risk — fixes were made directly in compiled artifacts (dist/), so changes will be lost on update; maintainers should apply the fix in source, add tests/guards for first-turn-only injection, and run post-update revalidation. Affected compiled files: dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js.", + "dedupe_summary": "Discord threads: runtime re-injects volatile thread metadata every turn (message_id, reply_to_id, timestamp, Thread starter, Chat history), causing context pollution and prompt cache bust." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core bug (per-turn re-injection of volatile Discord thread metadata and its impact) with concrete fields listed, no boilerplate or noise, so it will cluster correctly with similar reports." + }, + "usage": { + "input_tokens": 752, + "output_tokens": 686 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "Component: Discord threads. The runtime in the compiled dist treats Discord threads as needing volatile metadata injected every turn (message_id, reply_to_id, timestamp, Thread starter, Chat history), causing context pollution, repeated history, degraded performance, and prompt-cache busting with increased token costs.", + "solution_summary": "A local manual patch edited dist/pi-embedded-D6PpOsxP.js to stop injecting those volatile fields on every turn and only inject them on the first turn; validation shows repeated context stops, prompt caching is effective, and billed tokens drop significantly.", + "maintainer_signal_summary": "High regression risk: the fix was applied to a compiled dist file and will be overwritten by updates; maintainers should apply an official source-level fix and/or add a post-update revalidation script. Related issues: #41355, #34228, #37047.", + "dedupe_summary": "Discord threads: volatile thread metadata re-injected on every turn causing context pollution, token bloat and cache busting; temporary compiled-file patch made to inject metadata only on first turn." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: states the core bug (Discord thread metadata re-injected each turn), its impacts (context pollution, token bloat, cache busting), and notes the targeted fix without template noise, so it will cluster with similar reports." + }, + "usage": { + "input_tokens": 760, + "output_tokens": 517 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "Discord threads runtime had two regressions: per-turn reinjection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing progressive context growth and performance/latency degradation, and the session resolver falling back to agent:*:main instead of resolving live thread-bound sessions leading to incorrect CLI routing.", + "solution_summary": "Manual patches applied to compiled runtime bundles (modified dist/*.js files) to stop metadata reinjection and enforce resolver lookup by delivery context (channel+to+accountId); backups created and an idempotent re-application script added, gateway restarted and validated.", + "maintainer_signal_summary": "Fixes are manual edits to compiled dist bundles and will be lost on updates—maintainers must keep and run the provided re-apply script and backups; verification checklist passed and recommended non-regression tests (long-thread, 15–30min monitoring, smoke tests) should be added.", + "dedupe_summary": "[Discord threads]: Runtime patches to prevent per-turn metadata reinjection causing context growth and to fix session resolver so CLI routes to thread-bound sessions" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the two core runtime fixes (prevent per-turn metadata reinjection causing context growth and correct CLI session resolver to use thread-bound sessions) with no boilerplate or noise." + }, + "usage": { + "input_tokens": 736, + "output_tokens": 842 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "Discord message sending (REST) failing to deliver replies: OpenClaw receives messages and generates responses, but final reply fails with \"TypeError: fetch failed\" despite Discord proxy being configured and reported as enabled.", + "solution_summary": "Ensure the Discord REST client uses the configured proxy for outgoing reply fetch calls (apply proxy agent to fetch requests), add clearer logging around which proxy/agent is applied to REST requests, and implement retries or better error handling for fetch failures; add tests for proxy use in reply sending.", + "maintainer_signal_summary": "Regression: previously worked, now fails on OpenClaw 2026.3.2 with Node.js 22 on Linux systemd; proxy (http://127.0.0.1:7890) verified working via curl; logs show discord rest/gateway proxy enabled and login succeeded but replies consistently fail with \"final reply failed: TypeError: fetch failed\" in both DMs and group chats—high impact for Discord functionality.", + "dedupe_summary": "Discord message sending: outgoing replies fail with 'TypeError: fetch failed' despite discord proxy configured/enabled (incoming messages received and responses generated)" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (outgoing Discord replies failing with 'fetch failed' despite proxy being configured while incoming messages and responses are generated), with no boilerplate or noise." + }, + "usage": { + "input_tokens": 676, + "output_tokens": 683 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "Component: CLI session resolver. The CLI command `openclaw agent --channel discord --to ...` routes messages to `agent:*:main` instead of the live Discord channel/thread because the compiled resolver does not look up sessions by delivery context and does not normalize Discord targets.", + "solution_summary": "A manual patch was applied to the compiled file `dist/gateway-cli-BjsM6fWb.js` to normalize Discord targets (e.g., prefix `channel:`), perform session lookup by delivery context (channel+to+accountId) before falling back, and ensure CLI messages route to the correct live session; validations confirm correct routing.", + "maintainer_signal_summary": "High-risk manual fix applied to a compiled `dist/` artifact that will be lost on upgrade; needs a source-level fix and tests to prevent regression. Related issues: #36401 and #41562.", + "dedupe_summary": "CLI routing/session resolver: CLI --channel discord --to resolves to agent:*:main instead of the live Discord session because the resolver lacks delivery-context lookup and target normalization; patched compiled dist file as a stopgap." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: states that the CLI's Discord routing resolves to agent:*:main due to missing delivery-context lookup and target normalization and notes a patched dist file, which is exactly the core issue for deduplication." + }, + "usage": { + "input_tokens": 741, + "output_tokens": 854 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "Component: session resolver/compact bundle. A regression in the compiled file dist/compact-1mmJ_KWL.js calls a nonexistent resolveAgentMainSessionKey, causing ReferenceError at runtime, breaking the `agent` command and potentially crashing the gateway.", + "solution_summary": "Manual patch applied to dist/compact-1mmJ_KWL.js replacing resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...), which removes the ReferenceError and restores agent/sessions behavior; note this is a high-risk change to compiled files and will be lost on update.", + "maintainer_signal_summary": "This is a regression introduced by the prior session-resolver fix and must be fixed in source (not only in dist) to prevent recurrence; current fix is a temporary manual edit of compiled output and should be integrated into upstream code/release.", + "dedupe_summary": "session resolver (compact): ReferenceError from using nonexistent resolveAgentMainSessionKey instead of buildAgentMainSessionKey in dist/compact-1mmJ_KWL.js" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the root cause (wrong function name in compiled compact bundle causing ReferenceError) that will match similar issues and contains no boilerplate." + }, + "usage": { + "input_tokens": 712, + "output_tokens": 641 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "Discord channel plugin regression: when channels.discord.proxy is configured the proxy is only used for WebSocket connections, not for REST API calls or health checks. As a result the health check reports \"fetch failed\" and openclaw message send to Discord times out or fails in proxy-required environments despite curl via the same proxy working.", + "solution_summary": "Apply the channels.discord.proxy setting to the HTTP client used for Discord REST API calls and health checks (not just WebSocket), ensuring all Discord HTTP requests are routed through the configured proxy; update tests and health-check behavior to validate proxy usage.", + "maintainer_signal_summary": "Reporter marks this a regression and provides reproduction steps showing direct curl with the proxy succeeds while OpenClaw fails; includes version and environment details and labels bug/regression, indicating it breaks message delivery in proxy environments.", + "dedupe_summary": "Discord plugin: channels.discord.proxy is only applied to WebSocket connections, not REST API calls or health checks, causing fetch failures and message-send timeouts via proxy." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the plugin, exact config key, and the core bug (proxy applied only to WebSocket, not REST/health), which will cluster correctly with similar reports." + }, + "usage": { + "input_tokens": 668, + "output_tokens": 566 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "Session resolver for the agent command in the compact build (dist/compact-1mmJ_KWL.js) omits delivery-context lookup (channel+to+accountId), so sessions fall back to agent:*:main and agent responses are not associated with the live channel/thread session.", + "solution_summary": "A manual patch to dist/compact-1mmJ_KWL.js adds lookup by delivery context (channel + to + accountId) in the agent session resolver and only falls back to main when no specific session is found; validation shows sessions and routing are correct after the change.", + "maintainer_signal_summary": "High-regression risk: change was made directly in compiled dist file and will be lost on reinstall/update; requires a source-level fix and release. Patch validated locally but maintainers should implement the fix in source and publish a new build.", + "dedupe_summary": "session resolver (agent command, compact build): missing delivery-context lookup (channel+to+accountId) causing fallback to agent:*:main; manual dist patch added to perform delivery-context lookup." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies the affected component (agent command in compact build), the root cause (missing delivery-context lookup channel+to+accountId) and the symptom (fallback to agent:*:main), with only a minor note about a manual patch." + }, + "usage": { + "input_tokens": 682, + "output_tokens": 683 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English, creating usability barriers for non-English users (notably Chinese speakers) across settings, agent/skill management, channel configuration, and status messages.", + "solution_summary": "Proposed extraction of hardcoded strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, add a language selector or auto-detect, and build infrastructure to easily add more languages; notes that docs already have zh-CN translations and that control-ui bundles contain hardcoded strings.", + "maintainer_signal_summary": "This is a feature request with medium-high priority for UX accessibility; the author asks whether this is on the roadmap, offers contribution interest, and highlights specific files/bundles containing hardcoded strings.", + "dedupe_summary": "Control UI localization: Add i18n support and zh-CN translations by extracting hardcoded English strings into locale files, adding a language selector/auto-detect, and creating infrastructure for additional languages." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and focused summary that clearly captures the core change — add i18n/locales and zh-CN plus UI language selection — with no boilerplate or irrelevant details." + }, + "usage": { + "input_tokens": 603, + "output_tokens": 487 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "Component: WhatsApp channel/group prompt handling. Problem: WhatsApp group chats lacked support for channel-configured systemPrompt at account and group levels, preventing per-account and per-group prompt customization and wildcard defaults.", + "solution_summary": "Added systemPrompt support to WhatsApp config types/schema and implemented resolution logic: channel root or account-level systemPrompt (account overrides root), then resolve group prompt by specific group entry or '*' wildcard within the active groups map; account-level groups fully replace root groups. The final injected prompt is account prompt plus resolved group prompt (joined with a blank line). Tests and docs were added.", + "maintainer_signal_summary": "Scope limited to the WhatsApp channel only; no changes to other channels, WhatsApp hot-reload behavior, or group allowlist/mention-gating semantics. Includes tests and documentation and closes the related issue. Account-groups replace root groups (same semantics as Telegram).", + "dedupe_summary": "WhatsApp: add systemPrompt support at account and group levels with wildcard fallback and account-groups replacing root-groups" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary capturing the core change—adding per-account and per-group WhatsApp systemPrompt support with wildcard fallback—without extraneous template or testing details." + }, + "usage": { + "input_tokens": 2379, + "output_tokens": 708 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before agent processing, and WhatsApp group policy resolution could ignore account-scoped configuration causing allowlist gating to fail.", + "solution_summary": "Scope append processing to WhatsApp group messages only and propagate accountId when resolving WhatsApp group policy so account-scoped groupAllowFrom/allowFrom and groups fallback are considered, restoring correct inbound routing and gating.", + "maintainer_signal_summary": "DM append history remains skipped; no config/schema/dist changes; targeted local tests and live repro were used to verify behavior; files touched include web inbound and group-activation monitors and risks are documented for group backlog processing and account-scoped policy differences.", + "dedupe_summary": "WhatsApp group handling: enable append processing for group messages and use account-scoped accountId when resolving group allowlist/policy" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: clearly states enabling append processing for WhatsApp group messages and switching to account-scoped accountId for group allowlist/policy resolution without extraneous boilerplate." + }, + "usage": { + "input_tokens": 1931, + "output_tokens": 463 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Gateway (Canvas/Tailscale Serve): Canvas/A2UI returned 401 when proxied through Tailscale Serve because resolveClientIp returned undefined for trusted-proxy requests missing forwarding headers and isLocalDirectRequest did not fall back to the socket address when allowRealIpFallback was enabled.", + "solution_summary": "resolveClientIp now falls back to req.socket.remoteAddress when no forwarding headers exist; isLocalDirectRequest also checks the socket address when allowRealIpFallback is set; isLocalishHost now requires an allowTailscale flag to accept .ts.net hosts; auth.allowTailscale is threaded through callers and docs updated with required config.", + "maintainer_signal_summary": "Includes unit tests added/updated (src/gateway/auth.test.ts, net.test.ts) with passing results, docs changes, and an end-to-end verification matrix; closes #28472 and depends on #50636; no new config keys introduced and default behavior preserved for tailscale.mode = \"serve\", with noted risks and mitigations.", + "dedupe_summary": "Gateway: Fix Canvas 401 behind Tailscale Serve by falling back to socket address for trusted-proxy requests and gating .ts.net host acceptance behind allowTailscale." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core bug (401 behind Tailscale Serve) and the two code changes (socket fallback for trusted-proxy requests and gating .ts.net acceptance via allowTailscale) without boilerplate." + }, + "usage": { + "input_tokens": 1770, + "output_tokens": 503 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "Component: WhatsApp inbound access control computed an isSelfChat heuristic but never used it as a guard, so non-self messages (groups, stranger DMs, broadcasts) could pass through when groupPolicy defaulted to \"open\", violating self-chat isolation.", + "solution_summary": "Added an early-return guard in checkInboundAccessControl that rejects all non-self messages when account.selfChatMode === true, placed before group/DM policy evaluation; the isSelfChat heuristic is still returned for UI/logging but not used to block messages, and auto-detected self-chat remains unaffected.", + "maintainer_signal_summary": "All relevant tests pass (9 tests); author verified guard placement via code-path analysis and checked edge cases; decision to require explicit selfChatMode (not the heuristic) is intentional to avoid blocking teammates; quick revert instructions and affected file (src/web/inbound/access-control.ts) provided.", + "dedupe_summary": "WhatsApp inbound access control: add early-return guard to block non-self messages when explicit selfChatMode=true" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the code change: an early-return guard added to WhatsApp inbound access control to block non-self messages when explicit selfChatMode=true, with no boilerplate or extraneous detail." + }, + "usage": { + "input_tokens": 1485, + "output_tokens": 672 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "Component: WhatsApp group inbound handling. Problem: With groupPolicy: \"allowlist\", group messages from senders not in groupAllowFrom are dropped at the access-control layer and never stored as context, so agents lack conversation context from non-allowlisted members.", + "solution_summary": "Adds an opt-in boolean config groupContextFromAll (top-level and per-account) that, when true, marks non-allowlisted group messages as storeForContext/contextOnly instead of dropping them; these messages are saved to pending group history, do not trigger replies, and skip read receipts. Changes update access-control, inbound monitor, message types, auto-reply logic, and config schema.", + "maintainer_signal_summary": "Backwards-compatible default false; per-account config overrides top-level. Local testing with two bots verified context retention. Reviewer notes caution about potential allowlist normalization edge cases and interaction with group gating/state updates.", + "dedupe_summary": "WhatsApp group handling: add groupContextFromAll option to store non-allowlisted group messages as pending context instead of dropping them" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core change—adding a groupContextFromAll option to retain non-allowlisted WhatsApp group messages as pending context rather than dropping them—without extraneous boilerplate." + }, + "usage": { + "input_tokens": 1502, + "output_tokens": 489 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "Component: session management / sub-agent spawn. Current sessions_spawn always created a new random-UUID session for each sub-agent spawn, preventing reuse of conversation history and enabling session-key bypass edge cases.", + "solution_summary": "Added an optional sessionKey parameter to sessions_spawn that maps short keys to namespaced sessions (agent:{agentId}:subagent:{key}), accepts fully-qualified keys with agentId validation, and falls back to crypto.randomUUID() when omitted; tests added for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch; allowlist bypass fixed.", + "maintainer_signal_summary": "Reviewer flagged three actionable issues: a broken test import (openclaw-tools.js vs clawdbot-tools.js) causing test compilation failure, a case-sensitivity bug in fully-qualified key validation (embedded agentId not normalized causing false mismatches), and inconsistent error handling (throw vs jsonResult) for cross-agent mismatch checks—these should be fixed before merge.", + "dedupe_summary": "session management: add sessionKey param to sessions_spawn to enable deterministic sub-agent sessions (namespaced short keys, validated fully-qualified keys, UUID fallback); tests added but includes a failing import and validation/error-handling bugs." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary concisely captures the core change (sessionKey for deterministic sub-agent sessions, namespacing/validation, UUID fallback) but includes a testing/failing-import note that is implementation/test noise." + }, + "usage": { + "input_tokens": 1555, + "output_tokens": 636 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Telegram message editing: calling message(action=edit) on messages containing media (photo, video, audio, document, animation) invoked editMessageText and Telegram returned 400 'there is no text in the message to edit', causing edits to captionable messages to fail and leave UI inconsistent.", + "solution_summary": "editMessageTelegram (src/telegram/send.ts) now catches the specific 400 error (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption for captionable media; buttons-only updates call editMessageReplyMarkup directly; HTML parse fallback and MESSAGE_NOT_MODIFIED handling preserved; six unit tests added.", + "maintainer_signal_summary": "Bug fix limited to Telegram adapter (src/telegram/send.ts); backward compatible with no API surface changes; adds calls to editMessageCaption and editMessageReplyMarkup and a regex to detect the Telegram error—monitor regex stability; 6 tests added; revert by restoring the modified file.", + "dedupe_summary": "Telegram message editing: auto-detect captionable media edits and retry with editMessageCaption (buttons-only use editMessageReplyMarkup) to avoid 400 'no text' errors." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary specifying the exact Telegram API methods (editMessageCaption, editMessageReplyMarkup) and the 400 'no text' error, so it accurately captures the core issue for clustering." + }, + "usage": { + "input_tokens": 1436, + "output_tokens": 710 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "WhatsApp delivery: PR #24962 hardcoded disableBlockStreaming: true to block reasoning leaks, which unintentionally disabled block streaming entirely so messages were batched and sent only when the agent finished, ignoring per-account or global blockStreaming settings.", + "solution_summary": "The deliver callback was narrowed to only suppress tool payloads (reasoning is already filtered upstream) and disableBlockStreaming is now read from per-account channels.whatsapp.blockStreaming, falling back to agents.defaults.blockStreamingDefault, restoring progressive block streaming at configured boundaries.", + "maintainer_signal_summary": "Live testing and contract tests pass; change is backward compatible and can be reverted by setting channels.whatsapp.blockStreaming:false or removing the default; reviewers should watch for any regression of the upstream shouldSuppressReasoningPayload filter.", + "dedupe_summary": "WhatsApp delivery: restore config-driven block streaming by respecting channels.whatsapp.blockStreaming and agents.defaults.blockStreamingDefault and limiting deliver suppression to tool payloads." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it captures the exact behavioral change (restore config-driven block streaming for WhatsApp), names the relevant config keys, and notes the narrowed deliver suppression to tool payloads, with no boilerplate noise." + }, + "usage": { + "input_tokens": 1428, + "output_tokens": 458 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Telegram gateway integration: sending videos larger than the old 5MB default caused fetch/download to throw on content-length checks and left the undici Agent hanging in closeDispatcher() when the response body was unconsumed, making the gateway unresponsive and dropping messages with media.", + "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher() to avoid Agent.close() deadlock (timeout + destroy fallback), made media download errors non-fatal so captions/text are still forwarded, and added timeouts/retries to getFile and download operations; updated UI metadata stripping for inbound media paths.", + "maintainer_signal_summary": "E2E verified with a 7.5MB video (saved to ~/.openclaw/media/inbound and forwarded), tests updated (strip-inbound-meta.test.ts passes), change is backward-compatible and configurable, no SSRF policy change or new deps; touched src/infra/net/ssrf.ts and src/telegram/bot.ts; revertable by lowering channels.telegram.mediaMaxMb or reverting commits; noted risk of 3s closeDispatcher timeout but mitigated by destroy().", + "dedupe_summary": "Telegram integration: fix gateway hang on video messages by increasing mediaMaxMb to 20, preventing Agent.close() deadlock, making media download errors non-fatal, and adding timeouts/retries" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core bug and fixes (increase media limit, fix Agent.close deadlock, non-fatal media errors, timeouts/retries) without extraneous template or testing details." + }, + "usage": { + "input_tokens": 1524, + "output_tokens": 739 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "Telegram inbound body fallback used a hardcoded when media (e.g., documents) had no caption, causing documents and other non-photo media to be misidentified to the agent and leading to incorrect agent responses.", + "solution_summary": "Changed resolveTelegramInboundBody to use the already-computed type-aware placeholder (e.g., , ) instead of hardcoded , and added unit tests verifying placeholder text and getFile behavior for document messages; media download and extraction paths unchanged.", + "maintainer_signal_summary": "This is a small bugfix in the Telegram integration with unit tests added in extensions/telegram tests, closes #7116, backward compatible with minimal risk (noted downstream pattern-match caveat), and verified by test suite runs; labeled channel: telegram, size: S.", + "dedupe_summary": "Telegram inbound body: replace hardcoded fallback with type-aware media placeholder so document and other non-photo attachments are correctly labeled (e.g., ), plus unit tests to lock behavior." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core fix—switching the Telegram inbound body placeholder from a hardcoded to a type-aware placeholder (so documents/videos are labeled correctly) and adding tests—no extraneous boilerplate." + }, + "usage": { + "input_tokens": 1351, + "output_tokens": 516 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "Media store/gateway enforces a hardcoded 5 MB limit (MEDIA_MAX_BYTES) in src/media/store.ts, causing files larger than 5 MB to fail staging into sandboxed agents and preventing users from overriding the limit.", + "solution_summary": "Introduce a new config field media.maxBytes (optional, up to 500 MB), replace the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(), read config at gateway boot, and update error messages to reflect the configured limit while preserving the 5 MB default.", + "maintainer_signal_summary": "All relevant tests pass (56 tests across four files); three bot review threads were resolved (unconditional setting of limit, improved byte formatting, schema validation); change is backward-compatible (default preserved, deprecated export kept) and schema caps max at 500 MB to mitigate disk-fill risk.", + "dedupe_summary": "Media store: make MEDIA_MAX_BYTES configurable via media.maxBytes, replacing hardcoded 5MB with a configurable limit (default 5MB, max 500MB) and updating error messages." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change—making the media size limit configurable (keeps 5MB default, introduces up to 500MB) and updates related errors—without template noise." + }, + "usage": { + "input_tokens": 1472, + "output_tokens": 473 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "Telegram integration: when media/file downloads fail (exceeding mediaMaxMb or Telegram API errors), the handler returned early and the entire message was silently dropped—agents received no ack or visibility into the lost file or caption.", + "solution_summary": "On download failure, extract file metadata (name, size, type) from the Telegram message object and inject a metadata stub into msg.text instead of returning; allow the message to continue through the normal pipeline so the agent sees the file and reason it couldn't be downloaded.", + "maintainer_signal_summary": "Bug fix touching the Telegram integration only; backward compatible with no config changes. Verified via code inspection of early-return paths; risk noted that mutating msg.text may affect downstream consumers but is limited to prior-error cases. Can be reverted by rolling back the single commit to src/telegram/bot-handlers.ts; related issues linked.", + "dedupe_summary": "Telegram integration: on Telegram media download failure, inject file metadata into msg.text and continue processing instead of dropping the message" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states the integration, the failure mode (media download), and the exact behavior change (inject metadata into msg.text and continue processing) without extraneous boilerplate." + }, + "usage": { + "input_tokens": 1314, + "output_tokens": 522 + } + }, + { + "thread_id": 5796, + "number": 24117, + "kind": "pull_request", + "title": "feat(cli): add --session-key targeting for openclaw agent", + "summary": { + "problem_summary": "openclaw agent CLI lacks a --session-key option, causing CLI-triggered deliveries in multi-channel setups to resolve to the agent main session instead of the intended channel session because sessionId is UUID-based and --to may be unavailable for callbacks.", + "solution_summary": "Adds a first-class --session-key CLI flag, extends AgentCliOpts to include sessionKey, forwards it into resolveSessionKeyForRequest and gateway call parameters, preserves resolution precedence (--session-key, --session-id, --to, --agent), and updates help/validation text with accompanying tests.", + "maintainer_signal_summary": "Change is opt-in and backward compatible, includes unit tests for CLI parsing and gateway forwarding, judged low risk with a 5/5 confidence score, and requires no core resolver changes; documentation/help updated accordingly.", + "dedupe_summary": "[CLI agent command]: Add --session-key flag to target stable session keys for openclaw agent; forwarded through session resolution and prioritized over session-id/to/agent." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core change (adding a --session-key flag to openclaw agent and its precedence in session resolution) with relevant technical terms and no template noise." + }, + "usage": { + "input_tokens": 1286, + "output_tokens": 740 + } + }, + { + "thread_id": 766, + "number": 11611, + "kind": "pull_request", + "title": "feat: separate group-level allowlist from sender-level command authorization", + "summary": { + "problem_summary": "Component: WhatsApp inbound group access control. The current groupPolicy: \"allowlist\" combined with groupAllowFrom gates all group interactions by sender phone number, preventing allowlisting by group JID, allowing open chat within approved groups, and separating slash-command authorization from general chat access.", + "solution_summary": "When groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, access control uses the groups mapping to gate group-level access by JID (blocking unapproved groups) while allowing any participant in approved groups to chat; groupAllowFrom still controls slash-command authorization. If no groups mapping is present, the legacy sender-based groupAllowFrom behavior is preserved.", + "maintainer_signal_summary": "Change is localized to src/web/inbound/access-control.ts with added resolveChannelGroupPolicy usage, config JSDoc update, and four new Vitest cases. Tests cover approved/blocked groups and legacy fallback; flagged dependency on PR #3326; reviewer noted one outdated inline comment but rates change as safe to merge with confidence ~4/5.", + "dedupe_summary": "WhatsApp group access control: Use channels.whatsapp.groups JID allowlist for group-level gating when groupPolicy is \"allowlist\", while keeping groupAllowFrom for command authorization and preserving legacy sender-based behavior when no groups mapping exists." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change: switching to a group JID allowlist for group-level gating while retaining sender-based command authorization and legacy behavior when no groups mapping exists." + }, + "usage": { + "input_tokens": 1336, + "output_tokens": 613 + } + }, + { + "thread_id": 17924, + "number": 46425, + "kind": "pull_request", + "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", + "summary": { + "problem_summary": "Telegram outbound reply delivery ignored the configured mediaMaxMb limit: loadWebMedia was called without maxBytes and defaulted to 100 MB, so operators setting a lower mediaMaxMb saw no effect for replies (direct send already honored the setting).", + "solution_summary": "Threaded mediaMaxBytes through the Telegram outbound delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts), added unit tests to verify maxBytes is passed or omitted appropriately, and left direct-send and internals unchanged.", + "maintainer_signal_summary": "Bug fix, backward compatible (mediaMaxBytes optional), two unit tests added and related test suites passing (108 tests across 4 suites); build and checks pass but no live end-to-end Telegram verification was performed; change can be reverted by reverting the commit.", + "dedupe_summary": "Telegram outbound delivery: propagate mediaMaxMb/mediaMaxBytes to outbound media reply delivery so loadWebMedia enforces configured limit (threaded through bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts and native-commands path)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that states the core bugfix (propagating mediaMaxMb/Bytes so loadWebMedia honors the configured limit) and lists the affected files, with no template noise, so it will cluster correctly." + }, + "usage": { + "input_tokens": 1337, + "output_tokens": 620 + } + }, + { + "thread_id": 5712, + "number": 37050, + "kind": "pull_request", + "title": "fix(telegram): add HTML parse fallback for media captions", + "summary": { + "problem_summary": "Telegram media sends (photo, document, video, animation, audio, voice, video note) would fail with a 400 \"can't parse entities\" error when captions contained problematic HTML entities because they lacked the HTML parse fallback that text messages use.", + "solution_summary": "Wrapped the media send callback in withTelegramHtmlParseFallback to catch HTML parse errors and retry the send without parse_mode using the raw caption; applies to all media types and preserves existing caption splitting and other paths.", + "maintainer_signal_summary": "Added a unit test that simulates a parse error then success, verifying first call uses parse_mode: \"HTML\" and the retry omits parse_mode; all tests pass (52 total). Change is limited to src/telegram/send.ts and can be reverted by restoring the previous sendMedia implementation.", + "dedupe_summary": "Telegram media handler: Add HTML parse fallback for media captions to retry sends as plain text on HTML entity parse errors, preventing 400 failures." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that states the affected component (Telegram media captions), the change (HTML parse fallback to retry as plain text), and the impact (prevents 400 failures), so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 1211, + "output_tokens": 484 + } + }, + { + "thread_id": 21769, + "number": 55280, + "kind": "pull_request", + "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", + "summary": { + "problem_summary": "WhatsApp channel had block-streaming effectively disabled by a hardcoded disableBlockStreaming setting and an activation bug; additionally there was a delivery race where block replies were enqueued optimistically (sendBlockReply) so the pipeline marked content as sent before confirmed delivery, causing intermediate blocks and sometimes the final reply to be lost.", + "solution_summary": "Resolve blockStreaming from account config, fix the config activation logic, add sendBlockReplyAsync that waits for confirmed delivery, update onBlockReply to await it, add sensible block streaming coalescing defaults, and update/add tests to cover streaming-on and streaming-off paths.", + "maintainer_signal_summary": "Existing tests were updated and new tests added; manual verification confirms streaming works when enabled and remains off by default; introduces a new dispatcher API (sendBlockReplyAsync) and a new config key channels.whatsapp.blockStreaming, and fixes the root-cause race rather than reverting behavior.", + "dedupe_summary": "WhatsApp channel: make block streaming configurable and fix delivery race in block-reply pipeline" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies the WhatsApp channel change (make block streaming configurable) and the root fix (delivery race in block-reply pipeline), which is ideal for deduplication." + }, + "usage": { + "input_tokens": 1124, + "output_tokens": 865 + } + }, + { + "thread_id": 8098, + "number": 25543, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacks group-level allowlist config to explicitly allow specific Signal groups by ID, causing reliance on sender-level groupAllowFrom checks and preventing explicitly allowed groups from running control commands.", + "solution_summary": "Added a channels.signal.groups. config with a SignalGroupConfig type and Zod schema, wired resolveChannelGroupPolicy into the Signal event handler and command authorizers so listed groups bypass sender-level checks and can run control commands, and updated docs with examples.", + "maintainer_signal_summary": "PR fixes #25540 and supersedes #4337; Greptile review incorporated (groupExplicitlyAllowed now included in commandGate authorizers), TypeScript compiles cleanly, manual testing done, and changes are low-risk and consistent with other channels.", + "dedupe_summary": "Signal channel: add group-level allowlist support via channels.signal.groups. config that bypasses sender-level groupAllowFrom and enables command authorization for listed groups." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it names the new config path, the behavioral change (bypasses sender-level groupAllowFrom), and the impact on command authorization, so it will accurately cluster with similar Signal group-allowlist changes." + }, + "usage": { + "input_tokens": 1066, + "output_tokens": 484 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Component: Signal channel. Signal lacked group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs, group messages were silently dropped because isSenderAllowed compared sender phone/UUID to group IDs (sender-level filter mismatch).", + "solution_summary": "Added a groups config to SignalAccountSchemaBase (same shape as other channels), invoked resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom, and added four tests; all relevant test suites pass.", + "maintainer_signal_summary": "Small change scoped to the Signal channel; adds schema support and an access-check call, includes 4 new tests, fully tested and fixes issue #25540. Label: channel: signal, size: S.", + "dedupe_summary": "Signal: Missing group-level allowlist support via channels.signal.groups. causing group messages to be dropped when groupPolicy is allowlist; added groups config and resolveChannelGroupPolicy call to fix." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies Signal's missing group-level allowlist, the failure mode (group messages dropped under allowlist), and the fix (add groups config and resolveChannelGroupPolicy), with no boilerplate or noise." + }, + "usage": { + "input_tokens": 866, + "output_tokens": 460 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "WhatsApp channel hardcodes disableBlockStreaming=true in process-message, causing intermediate agent text blocks to be accumulated and sent as one final batch instead of delivered incrementally; queuedFinal was also used exclusively to mark delivery, so intermediate blocks weren't counted as replies.", + "solution_summary": "Add resolveWhatsAppBlockStreamingEnabled() and use the channels.whatsapp.blockStreaming config to control block streaming; update process-message logic so intermediate blocks count as valid replies when enabled; register blockStreaming capability and default coalescing constant in dock and update tests to cover enabled/default/disabled behavior.", + "maintainer_signal_summary": "Backwards-compatible default (blockStreaming off) preserved; tests updated and passing; small scoped change that aligns WhatsApp behavior with Telegram/Line and fixes anyReplyDelivered evaluation; labels indicate whatsapp-web, web-ui, agents, and size S.", + "dedupe_summary": "WhatsApp block streaming: make block streaming config-driven (respect channels.whatsapp.blockStreaming), stop hardcoding disableBlockStreaming, and deliver intermediate blocks as separate messages when enabled." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it captures switching WhatsApp from a hardcoded disableBlockStreaming to respecting channels.whatsapp.blockStreaming and delivering intermediate blocks, which is the core change for clustering." + }, + "usage": { + "input_tokens": 616, + "output_tokens": 623 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v6-negative-examples.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v6-negative-examples.json new file mode 100644 index 0000000..3f4d519 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v6-negative-examples.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v6-negative-examples", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "Summarize this GitHub issue or pull request. The dedupe_summary field will be used for vector-based duplicate detection.\n\nBAD dedupe_summary examples (too generic, would match unrelated issues):\n- \"Bug fix for an issue with the application\" ← too vague\n- \"Updates configuration handling and fixes a reported problem\" ← no specifics\n- \"Resolves issue where feature doesn't work as expected\" ← says nothing\n\nGOOD dedupe_summary examples (specific, would cluster correctly):\n- \"Discord REST API requests bypass proxy configuration, causing fetch failures in proxy-required environments\"\n- \"WhatsApp group-level systemPrompt configuration not applied to message delivery\"\n- \"Telegram editMessageCaption fails for non-photo media types due to missing content-type detection\"\n\nRemove from your analysis: template headers, checklists, testing instructions, environment details, reproduction steps, deployment notes. These add noise to embeddings.\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nPlain text, no markdown, 1-3 sentences each. dedupe_summary is the most critical field.", + "timestamp": "2026-03-30T17:41:02.635Z", + "aggregate": { + "avg_score": 4.78, + "avg_signal_density": 4.78, + "boilerplate_count": 1, + "boilerplate_pct": 3, + "would_cluster_correctly_pct": 100, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 47183, + "total_output_tokens": 26155 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "channels.discord.proxy only applies to the Discord WebSocket Gateway, not REST API startup requests (deploy commands, fetch-bot-identity) nor the guild resolve path. As a result, in environments that require an HTTP proxy the startup REST calls timeout with \"fetch failed\", guild: remains unresolved, and guild channels are non-functional while DMs (over WebSocket) still work.", + "solution_summary": "Ensure the configured channels.discord.proxy is applied to the undici dispatcher used for all Discord REST API calls during provider startup and the guild-resolve path (not just WebSocket). Consider adding a global proxy option that sets a ProxyAgent (setGlobalDispatcher) and add retry logic for guild resolution.", + "maintainer_signal_summary": "Regression reported on OpenClaw 2026.3.13 with reproduction steps, logs showing REST timeouts and guild unresolved, and verification that curl with the proxy works. User demonstrated a workaround by bootstrapping undici's ProxyAgent (NODE_OPTIONS) which fixes REST but not guild resolve, and suggests code change to route all Discord HTTP traffic through the proxy. Labels: bug, regression.", + "dedupe_summary": "channels.discord.proxy only proxies the Gateway websocket; Node v25/undici REST requests (deploy-rest, fetch-bot-identity) and the guild-resolve code path bypass the proxy causing \"fetch failed\" timeouts and guild channels to be unusable behind an HTTP proxy; global ProxyAgent workaround fixes REST but guild resolve still ignores the proxy." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summarizes the core bug (proxy only applied to Gateway, REST and guild-resolve bypass proxy causing failures) and notes workaround, but includes a Node/undici version detail that is unnecessary for deduping." + }, + "usage": { + "input_tokens": 2315, + "output_tokens": 669 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "With channels.discord.proxy configured, Discord gateway (WebSocket) connects but REST API sends fail with \"fetch failed\" in proxy-required environments (e.g., China). Logs indicate REST proxy enabled but the RequestClient/Carbon fetch implementation doesn't actually use the proxy, causing outbound REST/attachment uploads to fail.", + "solution_summary": "Make the RequestClient/Carbon fetch honor channels.discord.proxy by adding proxy support (HTTP/SOCKS agents or a proxy-aware fetch implementation), ensure attachment downloads/uploads and all REST calls route through the configured proxy, and add tests and configuration parsing so the proxy is consistently applied.", + "maintainer_signal_summary": "Please prioritize a fix to have the Discord REST client use the configured channels.discord.proxy (including attachments) or provide an official workaround; backport to the 2026.2.x line if possible and include tests to prevent regressions for proxy-required regions.", + "dedupe_summary": "channels.discord.proxy not applied to outbound REST sends because Carbon RequestClient's fetch has no proxy support, causing fetch failures in proxy-required environments while WebSocket gateway still works." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that identifies the core bug (channels.discord.proxy not applied to REST) and root cause (Carbon RequestClient fetch lacks proxy support), without boilerplate or noisy details." + }, + "usage": { + "input_tokens": 997, + "output_tokens": 544 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "When OpenClaw is configured with an HTTP proxy for Discord, the provider logs in and the gateway appears healthy but some startup REST calls consistently fail with \"fetch failed\", specifically native command deployment (/applications/{appId}/commands) and bot identity fetch, leaving the provider half-functional and triggering stale-socket health restarts.", + "solution_summary": "Ensure all Discord REST requests use the same proxy-aware fetch/dispatcher as the gateway/WebSocket path (unify transport code paths), and add retry/error handling for startup REST calls; user offered to provide environment details and test a patch.", + "maintainer_signal_summary": "Logs show rest proxy and gateway proxy both enabled, applicationId fetch succeeds but deploy-rest:put and fetch-bot-identity fail with \"fetch failed\" across repeated restarts; issue reproducible on OpenClaw 2026.3.12 (config touched by 2026.3.13) and reporter can share environment or test fixes.", + "dedupe_summary": "OpenClaw Discord provider behind HTTP proxy (channels.discord.proxy=http://127.0.0.1:8119) logs into gateway but startup REST calls—deploying native commands to /applications/{id}/commands and fetching bot identity—fail with \"fetch failed\", indicating inconsistent proxy handling between REST and gateway paths." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Summary concisely describes the core issue—Discord provider logs in via gateway behind an HTTP proxy while REST startup calls to deploy commands and fetch bot identity fail with \"fetch failed\", indicating inconsistent proxy handling—using specific config and endpoints useful for deduplication." + }, + "usage": { + "input_tokens": 1143, + "output_tokens": 694 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "Discord channel proxy is logged as enabled but Discord REST API calls fail with 'fetch failed' when OpenClaw runs under WSL2 with an HTTP proxy; the gateway WebSocket never establishes (gatewayConnected=false).", + "solution_summary": "Ensure the configured channels.discord.proxy is passed to the underlying HTTP client used by discord.js (undici/fetch) and to the WebSocket transport so REST requests and gateway connections use an HTTP proxy agent; update OpenClaw to create and supply a proxy agent or set the appropriate environment/undici agent on startup.", + "maintainer_signal_summary": "Reproducible 100% under WSL2 on OpenClaw 2026.3.13; logs show 'discord: rest proxy enabled' and 'discord: gateway proxy enabled' but REST calls return 'fetch failed' while curl via the same proxy succeeds. Severity: high (Discord channel unusable); config is ~/.openclaw/openclaw.json → channels.discord.proxy; workaround is TUN-mode transparent proxy or alternate channels.", + "dedupe_summary": "Discord proxy configured (channels.discord.proxy) and logged as enabled, but discord.js REST/undici fetch calls fail with 'fetch failed' and the gateway WebSocket never connects under WSL2, despite curl -x to discord.com working; proxy not being applied to the underlying HTTP/fetch/WebSocket client." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "The summary succinctly captures the core bug — the configured Discord proxy isn't being applied to the underlying undici/fetch/WebSocket clients causing REST 'fetch failed' errors and gateway connection failure — with specific tech terms and no template noise." + }, + "usage": { + "input_tokens": 980, + "output_tokens": 790 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "OpenClaw's Discord REST API calls for sending messages ignore the configured proxy and fail with \"TypeError: fetch failed\" while the Discord WebSocket and curl requests through the same proxy work fine, indicating the REST path isn't proxy-aware.", + "solution_summary": "Route Discord REST API calls through the same proxy-aware fetch/SSRF guard path introduced for web_search/web_fetch so they honor channels.discord.proxy and HTTP(S)_PROXY environment variables.", + "maintainer_signal_summary": "Logs show \"discord final reply failed: TypeError: fetch failed\"; repro with channels.discord.proxy and HTTP(S)_PROXY set; Telegram and Discord WebSocket use the proxy successfully and curl POST to the Discord API via the proxy succeeds; changelog notes web tools fixed proxy routing but excluded Discord REST; suggested fix is to reuse the shared proxy-aware fetch path; webhook workaround exists but doesn't support replies.", + "dedupe_summary": "Discord REST API (message send) in OpenClaw bypasses proxy-aware fetch path and fails with \"TypeError: fetch failed\" despite channels.discord.proxy and HTTP(S)_PROXY being set; WebSocket and curl via the proxy succeed, and the 2026.2.26 proxy fix for web_search/web_fetch did not include Discord REST." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that pinpoints Discord REST message sends bypassing the proxy-aware fetch path and failing with a TypeError despite proxy settings, so it will correctly cluster with similar proxy-related Discord REST issues." + }, + "usage": { + "input_tokens": 1003, + "output_tokens": 723 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the CLI command `openclaw agent --channel discord --to ...` resolves and delivers to the global agent:*:main session instead of the live Discord channel/thread session because the CLI session resolver does not look up sessions by real delivery context and Discord targets are not normalized.", + "solution_summary": "A manual patch to compiled dist files was applied: session resolution now searches by delivery context (channel + to + accountId), Discord target IDs are normalized (e.g. '148...' → 'channel:148...'), the compact resolver now uses buildAgentMainSessionKey and looks up delivery context, and the agent register command path now passes channel and replyAccount/accountId.", + "maintainer_signal_summary": "HIGH risk: changes were made directly in compiled files under dist and will be lost on update; maintainers should port fixes to source, add tests for delivery-context session resolution and Discord target normalization, and add post-update validation (or a persistent CLI --session-key option) to prevent regressions.", + "dedupe_summary": "CLI `openclaw agent --channel discord --to` routes to agent:*:main because the session resolver doesn't search by delivery context (channel+to+accountId), the command path didn't propagate channel/replyAccount, and Discord targets weren't normalized (e.g. '148...' vs 'channel:148...'); manual fixes applied to dist/gateway-cli-BjsM6fWb.js, dist/compact-1mmJ_KWL.js, and dist/register.agent-DHoJWl4M.js to add delivery-context lookup and normalization." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that states the exact CLI command misrouting, the root cause (missing delivery-context lookup, command-path propagation, and target normalization), and the files changed, which is sufficient to cluster with similar session-resolver/delivery-context bugs." + }, + "usage": { + "input_tokens": 930, + "output_tokens": 869 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions: Discord thread messages were re-injecting volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) every turn, causing progressive context bloat; and CLI agent commands (openclaw agent --channel discord --to ...) resolved to the default main session (agent:*:main) instead of the live thread-bound session.", + "solution_summary": "Patched compiled runtime bundles to stop injecting the volatile Discord thread metadata, added session resolution by actual delivery target with normalized Discord identifiers, updated the agent command session resolver to look up delivery context (channel + to + accountId), and passed channel and replyAccount/accountId into the resolver; also fixed incorrect session key usage.", + "maintainer_signal_summary": "Changes were applied directly to compiled files in openclaw/dist/ (dist/pi-embedded-*.js, dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js) and will be lost on updates; maintainers should integrate fixes into source, rebuild distribution artifacts, and add a post-update re-application script or permanent release to avoid regression.", + "dedupe_summary": "Discord threads repeatedly re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) each turn causing context degradation, and CLI routing (openclaw agent --channel discord --to ...) resolved to agent:*:main because the session resolver didn't use delivery context (channel+to+accountId); fixes: prevent metadata injection, normalize Discord targets, add delivery-context lookup in agent resolver, pass channel/replyAccount, and correct session key construction in compiled dist/*.js." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary of two related regressions: Discord thread metadata being re-injected each turn (listing exact fields) causing context bloat, and CLI agent commands resolving to the wrong session because the resolver ignored delivery context (channel+to+accountId), with targeted fixes noted." + }, + "usage": { + "input_tokens": 849, + "output_tokens": 732 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "The sandbox media staging cap is hardcoded to 5 MB in dist/store-*.js (const MEDIA_MAX_BYTES = 5 * 1024 * 1024), so files larger than 5 MB are silently rejected or unavailable inside sandboxed agents. Issue present in v2026.3.7 and v2026.3.8.", + "solution_summary": "Read MEDIA_MAX_BYTES from configuration (e.g. openclaw.json under tools.media.maxBytes) instead of a hardcoded constant, aligning with per-type keys like tools.media.image.maxBytes and tools.media.audio.maxBytes; update code to use the configured limit and provide a sensible default.", + "maintainer_signal_summary": "Labels: bug, regression. Affects OpenClaw v2026.3.8 on Ubuntu 24 installed via npm. Reproducible by sending files >5 MB to sandbox.mode non-main. Current workaround: post-install sed patch to replace the constant in dist/store-*.js and restart the gateway.", + "dedupe_summary": "Sandbox media staging limit MEDIA_MAX_BYTES is hardcoded to 5*1024*1024 in dist/store-*.js, causing files over 5 MB to be rejected; request to make tools.media.maxBytes configurable via openclaw.json (issue seen in v2026.3.7 and v2026.3.8)." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise summary captures the hardcoded MEDIA_MAX_BYTES, file location, and requested tools.media.maxBytes config change — the only minor noise is the unnecessary version numbers which aren't needed for deduping." + }, + "usage": { + "input_tokens": 896, + "output_tokens": 614 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "Discord bot receives messages over the Gateway but all Discord REST API calls fail with 'fetch failed' despite proxy configured in openclaw.json and logs showing 'rest proxy enabled'; direct curl calls through the same proxy succeed, so the proxy itself works.", + "solution_summary": "No resolution provided in the issue; user suggests REST client is not using the configured proxy. Investigation needed into how the Discord REST client/fetch is handling per-provider proxy settings (no workaround or patch included).", + "maintainer_signal_summary": "Reporter supplied config, logs and reproduction details (OpenClaw 2026.2.25 on macOS, Node 25.6.1); logs show 'rest proxy enabled' and gateway proxy enabled; no maintainer response or patch in the thread yet.", + "dedupe_summary": "Discord REST API requests return 'fetch failed' even though openclaw.json has 'proxy' and logs print 'rest proxy enabled'; Gateway WebSocket and Telegram (same proxy) work, and curl to Discord API via the same proxy succeeds — suggests Discord REST client in OpenClaw 2026.2.25 (Node 25.6.1, macOS) is not honoring proxy configuration." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary clearly states the core bug (Discord REST requests failing with 'fetch failed' despite proxy enabled and other clients working) but includes minor noisy details (specific OpenClaw/Node/macOS version) that aren't needed for deduplication." + }, + "usage": { + "input_tokens": 841, + "output_tokens": 760 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "Discord REST API calls made by @buape/carbon's RequestClient ignore the channels.discord.proxy entry in openclaw.json — only the WebSocket gateway respects the proxy. The client calls globalThis.fetch without a proxy-aware undici dispatcher, so REST requests (and memorySearch/attachment downloads) bypass the configured HTTP proxy on Node 22.", + "solution_summary": "Modify @buape/carbon's REST client to accept an undici dispatcher or explicit proxy configuration and have OpenClaw pass channels.discord.proxy to it so REST calls use the same proxy as the gateway. Short-term workaround: preload a proxy agent (NODE_OPTIONS=--require=proxy-preload.cjs) to call setGlobalDispatcher(new EnvHttpProxyAgent()), but avoid --use-env-proxy which breaks WebSocket upgrades.", + "maintainer_signal_summary": "Blocking for proxy-required deployments: gateway works but REST/embeddings/media downloads bypass proxy. A preload workaround exists; do not use NODE_OPTIONS=--use-env-proxy because it causes Discord WebSocket disconnections. Related: issue #25232 (attachment downloads bypass proxy).", + "dedupe_summary": "@buape/carbon RequestClient invokes globalThis.fetch without supplying an undici dispatcher, causing Discord REST API calls to ignore openclaw.json channels.discord.proxy; Node 22's native fetch doesn't honor HTTP_PROXY/HTTPS_PROXY without --use-env-proxy (which in turn breaks WebSocket connections)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technically specific summary that identifies the root cause (RequestClient using globalThis.fetch without an undici dispatcher causing channels.discord.proxy to be ignored) and relevant Node 22 proxy behavior, so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 850, + "output_tokens": 876 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "OpenClaw shows performance degradation under sustained load with long-running conversations: unbounded context growth (writer ~42k tokens, reasoning ~90k tokens) increasing per-turn latency, Discord lane blocking causing serialization under load, and typing indicators stuck until TTL in some code paths.", + "solution_summary": "Applied configuration limits (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, agents.defaults.subagents.maxConcurrent 8→4, agents.defaults.subagents.maxChildrenPerAgent 8→4, agents.defaults.contextTokens 80000) and increased channels.discord.eventQueue.listenerTimeout 120000→300000; improved patching script to cover hash variants; restarted gateway and observed no new \"lane wait exceeded\" or \"typing TTL reached\" errors, though restart is not a full fix under sustained load. Recommendation: make limits adaptive or expose them in openclaw config.", + "maintainer_signal_summary": "Maintainers lowered concurrency limits, imposed a 80k token context cap, extended Discord event queue listener timeout, and updated openclaw-rootfix-v2026.3.11.sh to patch all reply/compact/pi-embedded/gateway-cli/register.agent hash variants; gateway restart validated immediate symptom reduction but long-term mitigation requires config tuning.", + "dedupe_summary": "Unbounded context token growth (writer ~42k, reasoning ~90k) causing rising latency, Discord lane blocking and stuck typing; mitigated by reducing concurrency (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents 8→4), setting agents.defaults.contextTokens=80000, and increasing channels.discord.eventQueue.listenerTimeout 120000→300000; script hardening in openclaw-rootfix-v2026.3.11.sh." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary captures the core technical problem (unbounded context growth causing latency, lane blocking, stuck typing) and remedial configuration changes, but includes minor noisy details (exact config values and a script/version name) that aren't necessary for deduplication." + }, + "usage": { + "input_tokens": 784, + "output_tokens": 830 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord threads repeatedly re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every conversation turn, causing context contamination, ballooning token usage, prompt-cache busting and performance degradation in long threads.", + "solution_summary": "A manual hotfix was applied to compiled dist files: dist/pi-embedded-D6PpOsxP.js prevents re-injection of the listed Discord thread fields, and dist/compact-1mmJ_KWL.js adds an isNewSession guard for thread starter and replaces resolveAgentMainSessionKey with buildAgentMainSessionKey(...); validation shows thread context no longer repeats and prompt caching is restored.", + "maintainer_signal_summary": "High risk: changes were made directly to compiled dist/ files and will be lost on update/reinstall. Recommended actions: implement the fix in source, add the isNewSession guard in the session handler, correct session key construction, and add post-update revalidation to catch regressions; refer to related issues #41355, #34228, #37047.", + "dedupe_summary": "Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) every turn, corrupting context and busting prompt cache; temporary fix applied in dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js to stop injection and add an isNewSession guard, but the change must be ported to source to persist." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the root cause (Discord thread metadata re-injected each turn causing context pollution and cache bust) and the temporary dist-file fix, with no template noise, so it will cluster well with similar issues." + }, + "usage": { + "input_tokens": 796, + "output_tokens": 711 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord thread messages have volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) re-injected on every turn, causing context pollution, performance degradation in long threads, and prompt cache busting with higher token costs.", + "solution_summary": "A manual patch was applied to the compiled file dist/pi-embedded-D6PpOsxP.js to stop re-injecting those Discord thread metadata fields after the first turn; validation shows repeated context stops, prompt caching is effective again, and billed tokens drop significantly. The recommended next step is to implement a source-level fix and keep a post-update revalidation script.", + "maintainer_signal_summary": "High regression risk: the fix was made directly in compiled dist files and will be lost on update/reinstall. Maintainters should port the patch into source, release an official fix, and consider automated revalidation after updates. Related issues: #41355, #34228, #37047.", + "dedupe_summary": "dist/pi-embedded-D6PpOsxP.js re-injects Discord thread volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, token bloat and prompt cache invalidation; manual dist patch restricts injection to the first turn." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that names the affected compiled file, the exact metadata re-injected, the impact (context pollution/token bloat/cache invalidation), and the intended fix scope (restrict to first turn), so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 804, + "output_tokens": 557 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "Two runtime regressions in Discord thread handling: per-turn reinjection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing unbounded context growth and performance/latency degradation, and the session resolver incorrectly falling back to agent:*:main instead of resolving live thread-bound sessions, causing CLI routing to the wrong session.", + "solution_summary": "Applied direct runtime patches to compiled bundles (dist/reply-BCcP6j4h.js, dist/compact-1mmJ_KWL.js, dist/pi-embedded-Cz5VjpnY.js, dist/gateway-cli-B-E8XzUM.js, dist/register.agent-jS8B5meo.js) to stop per-turn metadata reinjection and make resolver lookups consistent by delivery context (channel+to+accountId); added idempotent re-application script and backups, restarted and validated the gateway.", + "maintainer_signal_summary": "Patches are manual edits to compiled bundles and will be lost on updates—maintainers must run the provided re-application script and keep backups (*.bak-rootfix-*); gateway restart and log checks passed and a verification checklist and recommended smoke/long-thread tests are documented.", + "dedupe_summary": "Runtime patches to compiled dist JS bundles to remove per-turn reinjection of message_id/reply_to_id/timestamp/Thread starter/InboundHistory that caused context growth in long Discord threads, and to fix session resolver lookup to use delivery context (channel+to+accountId) preventing fallback to agent:*:main; includes idempotent reapply script and backup files." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise and specific about the two core fixes (removing per-turn metadata reinjection causing context growth and fixing session resolver to use delivery context), with only a minor operational detail about reapply scripts that isn't necessary for deduping." + }, + "usage": { + "input_tokens": 780, + "output_tokens": 708 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "OpenClaw 2026.3.2 fails to send Discord reply messages with \"TypeError: fetch failed\" even though incoming messages are received and logs indicate both REST and gateway proxies are enabled. The configured HTTP proxy (http://127.0.0.1:7890) works for curl, but sending replies from the bot always fails in systemd-run Node 22 environment.", + "solution_summary": "No patch provided in the report; reporter suggests the Discord REST client is not applying the configured proxy when sending replies. Likely fix: ensure the fetch/http client used for Discord REST reply requests uses the configured proxy agent or proxy environment variables so outbound requests go through the Clash proxy.", + "maintainer_signal_summary": "This is a regression (worked previously) and blocks outgoing replies while receives and model responses succeed; reproducible under systemd with Node 22 and OpenClaw 2026.3.2. Logs show 'discord: rest proxy enabled' and 'discord: gateway proxy enabled' yet final reply fails, indicating investigation into how fetch is invoked for reply sends is required.", + "dedupe_summary": "OpenClaw 2026.3.2: Discord replies fail with 'final reply failed: TypeError: fetch failed' despite 'discord: rest proxy enabled' and an http://127.0.0.1:7890 Clash proxy configured; incoming messages and model responses work and curl via the proxy succeeds, implying Discord REST reply requests are not using the configured proxy (regression with Node 22, systemd)." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary accurately states the core bug (Discord REST replies failing despite proxy config and incoming messages working) but includes minor noise (version/Node/systemd details) that aren't needed for deduplication." + }, + "usage": { + "input_tokens": 720, + "output_tokens": 705 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the CLI command `openclaw agent --channel discord --to ...` routes messages to the generic session `agent:*:main` instead of the live Discord channel/thread session because the compiled session resolver doesn't lookup by delivery context and Discord targets aren't normalized.", + "solution_summary": "A local manual patch was applied to dist/gateway-cli-BjsM6fWb.js to normalize Discord targets (prefixing with 'channel:'), add session lookup by delivery context (channel + to + accountId), and only fallback to agent:*:main if no matching session is found; this makes CLI routing hit the correct live session.", + "maintainer_signal_summary": "High regression risk: change was made directly in compiled dist/ file and will be lost on update; fix needs to be implemented in source, covered by tests, and coordinated with related issues (#36401, #41562) to avoid reverting behavior.", + "dedupe_summary": "CLI routing bug: session resolver in dist/gateway-cli-BjsM6fWb.js does not lookup by delivery context (channel+to+accountId) and fails to normalize Discord target IDs (e.g., '148...' vs 'channel:148...'), causing fallback to agent:*:main; patched by adding normalization and delivery-context session lookup." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the core bug (session resolver in dist/gateway-cli not using delivery-context lookup and failing to normalize Discord targets, causing fallback to agent:*:main) and would match similar routing/normalization issues." + }, + "usage": { + "input_tokens": 785, + "output_tokens": 613 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 a regression in the compiled file dist/compact-1mmJ_KWL.js calls a non-existent resolveAgentMainSessionKey(), causing ReferenceError at runtime and breaking the agent command and gateway processing.", + "solution_summary": "Manually patched dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...), restoring the session resolver and eliminating the ReferenceError; validation shows agent commands and new writer/reasoning sessions work correctly.", + "maintainer_signal_summary": "This is a high-risk regression introduced by a prior fix (fix #3) and fixed only in compiled dist files; maintainers must apply the correction in source (not just dist) before reapplying or releasing to avoid the ReferenceError being reintroduced.", + "dedupe_summary": "dist/compact-1mmJ_KWL.js calls undefined resolveAgentMainSessionKey instead of the available buildAgentMainSessionKey, causing ReferenceError and agent command failures; manual hotfix replaces the call in the compiled bundle but will be lost on reinstall/upstream updates." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary that identifies the exact faulty compiled file, the incorrect function call causing a ReferenceError, and that a local hotfix exists but will be overwritten, which is ideal for deduplication." + }, + "usage": { + "input_tokens": 756, + "output_tokens": 527 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "Discord plugin's REST API requests and health check ignore channels.discord.proxy (only WebSocket uses it), causing 'fetch failed' and message send timeouts in proxy-required environments despite curl via the proxy working.", + "solution_summary": "Patch the Discord channel to apply channels.discord.proxy to all HTTP/REST API calls and health checks (not just WebSocket), ensure the gateway passes the proxy to the HTTP/fetch client, add tests and release a fix.", + "maintainer_signal_summary": "Regression reported on OpenClaw 2026.3.8; labeled bug/regression. Reporter requests urgent fix so REST calls and health check respect channels.discord.proxy and message delivery works behind a proxy.", + "dedupe_summary": "Discord REST API requests and health check bypass channels.discord.proxy (only WebSocket uses the proxy), causing 'fetch failed' and message send timeouts in proxy-required environments." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that focuses on the core bug — channels.discord.proxy not applied to REST API/health checks (only WebSocket), causing fetch failures — with no extraneous template text." + }, + "usage": { + "input_tokens": 712, + "output_tokens": 594 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "En OpenClaw v2026.3.11 el session resolver del comando agent dentro de dist/compact-1mmJ_KWL.js no realiza lookup por delivery context (channel+to+accountId), por lo que resuelve sesiones a agent:*:main en vez de la sesión viva del canal/target; provoca inconsistencia entre routing del CLI gateway y el comando y respuestas mal asociadas a sesiones.", + "solution_summary": "Parche manual en dist/compact-1mmJ_KWL.js: se añadió lookup por delivery context (combinación channel + to + accountId) en el session resolver y se mantiene fallback a main solo si no se encuentra sesión específica; validado que el agent resuelve y asocia respuestas correctamente.", + "maintainer_signal_summary": "Parche aplicado directamente en archivos compilados en dist/ (high risk): será sobrescrito en actualizaciones/reinstalaciones; se requiere corrección en la fuente/build para persistir el arreglo y evitar regresiones en futuras releases.", + "dedupe_summary": "dist/compact-1mmJ_KWL.js (OpenClaw v2026.3.11) session resolver for agent command lacks delivery-context lookup (channel+to+accountId), causing fallback to agent:*:main and incorrect session association; manual compiled-file patch adds delivery-context lookup and fallback to main only if no specific session found." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Clearly and specifically states the core bug (compiled session resolver missing delivery-context lookup causing fallback to agent:*:main) and the resulting patch, giving strong, focused signal for deduplication." + }, + "usage": { + "input_tokens": 726, + "output_tokens": 472 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "The OpenClaw Control UI contains most interface text hardcoded in English (settings, buttons, agent/skill pages, channel configs, status messages), creating usability barriers for non-English users, particularly Chinese speakers.", + "solution_summary": "Extract hardcoded strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translated strings, implement a language selector or auto-detect, and build reusable infrastructure to add more languages.", + "maintainer_signal_summary": "Feature request marked Medium-High priority for UX accessibility; notes docs already have zh-CN translations and that the built UI bundle contains hardcoded strings; asks whether this is on the roadmap or open for contributors.", + "dedupe_summary": "Control UI hardcoded English strings; request to implement i18n starting with Simplified Chinese (zh-CN) by extracting strings to locale files, adding translations, and providing a language selector/auto-detect." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core feature request (implement i18n for Control UI, extract strings, add zh-CN translations, and language selector/auto-detect) with no boilerplate or extraneous details." + }, + "usage": { + "input_tokens": 647, + "output_tokens": 395 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "WhatsApp group chats couldn't apply channel-level systemPrompt settings at account or group granularity, preventing per-account and per-group prompt customization (including wildcard defaults) and causing unclear override semantics between root and account group maps.", + "solution_summary": "Adds systemPrompt support to WhatsApp config schema and resolution logic, supporting channels.whatsapp.systemPrompt, channels.whatsapp.accounts..systemPrompt, channels.whatsapp.groups..systemPrompt and account-level groups with wildcard '*'; implements account-over-root precedence, group-specific then wildcard fallback, account groups fully replace root groups, plus tests and docs.", + "maintainer_signal_summary": "Closes issue #7011 and replaces the author's prior larger PR; backward compatible with no security/network changes, limited scope to WhatsApp gateway/orchestration and integration code, includes tests/docs, and does not alter hot-reload or non-WhatsApp behavior (size M).", + "dedupe_summary": "Introduce WhatsApp systemPrompt hierarchy: channels.whatsapp.systemPrompt fallback, overridden by channels.whatsapp.accounts..systemPrompt; resolve group prompt from accounts..groups. then '*' wildcard; account 'groups' map fully replaces root channels.whatsapp.groups (no merge); final injected prompt = account prompt + resolved group prompt joined with a blank line." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the new WhatsApp systemPrompt resolution hierarchy, wildcard/group semantics, replacement behavior, and final prompt composition—no boilerplate or irrelevant details." + }, + "usage": { + "input_tokens": 2423, + "output_tokens": 889 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before agent processing, and group policy resolution omitted account-scoped data (no accountId), causing account-scoped groupAllowFrom/allowFrom and allowlist fallback to be ignored.", + "solution_summary": "Enable append processing only for WhatsApp group messages (DM append history still skipped) and propagate accountId when resolving WhatsApp group policy so account-scoped channels.whatsapp.accounts. and its groupAllowFrom/allowFrom are considered for gating and allowlist fallback.", + "maintainer_signal_summary": "Changes touch src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts; no config/schema/dist changes, no debug logging added. Targeted tests and live traces verify groups now route and gate correctly; revert by restoring the two files if needed. Risks: processing backlog/history for groups may change behavior, but DM append remains skipped and tests cover account-scoped allowlist behavior.", + "dedupe_summary": "WhatsApp group append events were skipped before enqueueing and group policy helpers resolved without accountId, causing account-scoped channels.whatsapp.accounts. groupAllowFrom/allowFrom fallback to be ignored; fix scopes append handling to groups and uses account-scoped account for allowlist fallback." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core bug and the fix: append handling for WhatsApp groups was skipped and policy resolution lacked account-scoping, now both are corrected to use group-scoped append processing and account-scoped allowlist fallback." + }, + "usage": { + "input_tokens": 1975, + "output_tokens": 716 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Canvas (A2UI) returned 401 when accessed through Tailscale Serve because resolveClientIp() returned undefined for requests from a trusted proxy with no forwarding headers, and isLocalDirectRequest() did not fall back to the socket address when allowRealIpFallback was enabled and XFF resolved to a non-loopback Tailscale IP.", + "solution_summary": "Fixes return the socket address from resolveClientIp() when no forwarding headers are present, make isLocalDirectRequest() check req.socket.remoteAddress when allowRealIpFallback is enabled, gate .ts.net host acceptance behind an existing auth.allowTailscale flag, thread allowTailscale through callers, and update docs and tests to reflect required gateway.trustedProxies and allowRealIpFallback settings.", + "maintainer_signal_summary": "Includes unit tests (auth/net), docs changes, and depends on PR #50636 (merge first); behavior is backward-compatible since allowTailscale defaults to true for tailscale.mode = \"serve\", and reviewers should inspect auth/net and server-http related files for the socket-fallback and .ts.net gating logic.", + "dedupe_summary": "Canvas returns 401 behind Tailscale Serve because resolveClientIp returned undefined for direct trusted-proxy requests without X-Forwarded headers and isLocalDirectRequest didn’t fallback to req.socket.remoteAddress when allowRealIpFallback is true; fix adds socket-address fallback, gates .ts.net host-header acceptance behind auth.allowTailscale, and threads allowTailscale through callers." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the root causes (resolveClientIp and isLocalDirectRequest fallbacks), the concrete fixes (socket fallback, .ts.net gating, threading allowTailscale), and uses precise technical terms useful for deduplication." + }, + "usage": { + "input_tokens": 1814, + "output_tokens": 676 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "checkInboundAccessControl() computed an isSelfChat heuristic but never used it as a guard, so when WhatsApp channel was configured without an explicit groupPolicy non-self messages (groups, DMs from strangers, broadcasts) could pass through because resolveWhatsAppRuntimeGroupPolicy() defaulted to \"open\". This violated self-chat mode's isolation intent by allowing non-self messages to reach downstream processing.", + "solution_summary": "Added an early-return guard in checkInboundAccessControl() that immediately rejects any non-self message when account.selfChatMode === true, placed before group/DM policy evaluation. The change intentionally checks only the explicit selfChatMode flag (not the heuristic isSelfChat) to avoid blocking teammates allowed via allowFrom; isSelfChat is still returned for UI/logging.", + "maintainer_signal_summary": "All existing access-control tests (9) pass; code-path analysis and targeted verifications were done and the guard placement is intentional. Change affects only WhatsApp inbound access-control (src/web/inbound/access-control.ts) and can be reverted by removing the early-return block; note that auto-detected self-chat (heuristic-only) remains unblocked by design.", + "dedupe_summary": "WhatsApp access-control bug: explicit selfChatMode=true did not block non-self messages because isSelfChat was computed but unused and groupPolicy defaulted to \"open\", allowing groups/strangers through; fix adds an early-return guard rejecting non-self messages when account.selfChatMode === true while preserving teammate allowFrom behavior." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that states the core WhatsApp self-chat access-control bug, root cause (unused isSelfChat + groupPolicy default), and the precise fix (early-return guard on account.selfChatMode) without boilerplate." + }, + "usage": { + "input_tokens": 1529, + "output_tokens": 678 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "With groupPolicy set to \"allowlist\", group messages from senders not in groupAllowFrom are dropped at access-control (allowed: false → continue) so the agent never sees them in the \"Chat messages since your last reply\" context block, breaking the documented flow where unmentioned messages should be stored for context.", + "solution_summary": "Adds an opt-in config flag groupContextFromAll (top-level and per-account, default false) that makes access-control return storeForContext for non-allowlisted senders, marks those inbound messages as contextOnly (no read receipts), and records them as pending group history so the agent sees them for context but they cannot trigger replies.", + "maintainer_signal_summary": "Change is opt-in and backwards-compatible (default false), small scope touching access-control, monitor, types, config schema and account resolution; labeled channel: whatsapp-web, size: S, stale. Greptile flagged a 3/5 confidence due to a potential allowlist normalization/matching edge case and the new path bypassing some group gating/state updates.", + "dedupe_summary": "WhatsApp allowlist behavior drops non-allowlisted group messages entirely; this PR introduces groupContextFromAll to store those messages as pending context (contextOnly) without triggering replies, threads a new access-control outcome (storeForContext) through inbound monitor, skips read receipts, and persists entries in group history (per-account or top-level config, default false)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary of the core bug (allowlist discards group messages) and the concrete fix (new groupContextFromAll config, storeForContext outcome, context-only storage, skip receipts, persist history) with specific terms that will match duplicates." + }, + "usage": { + "input_tokens": 1546, + "output_tokens": 786 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "sessions_spawn always created a new random-UUID session for sub-agents, preventing deterministic session reuse and preserving conversation history across spawns; there was also a Greptile finding about a session key bypassing an allowlist.", + "solution_summary": "Adds an optional sessionKey parameter: short keys map to agent:{agentId}:subagent:{key}, fully-qualified keys are accepted after validating the embedded agentId matches targetAgentId, and omission falls back to crypto.randomUUID(); includes four tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch rejection.", + "maintainer_signal_summary": "Reviewer flagged three blocking issues: a test imports a non-existent module (openclaw-tools.js vs clawdbot-tools.js) causing compilation failure, a case-sensitivity bug in fully-qualified agentId validation (embedded ID not normalized), and inconsistent error handling (throw vs returning jsonResult), lowering confidence for merge.", + "dedupe_summary": "Introduce optional sessionKey to sessions_spawn to reuse sub-agent sessions: short keys namespace to agent:{agentId}:subagent:{key}, fully-qualified keys validated to prevent cross-agent session injection, fallback to crypto.randomUUID(); tests added; problems found—broken test import, case-sensitive agentId mismatch bug, and inconsistent error handling." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "The summary clearly describes the new optional sessionKey behavior, namespacing, validation, and fallback which are the core signals for deduplication, but it includes extra noise about broken tests and minor bugs that aren't needed for clustering." + }, + "usage": { + "input_tokens": 1599, + "output_tokens": 628 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) used editMessageText and caused Telegram to return 400 \"there is no text in the message to edit\", so message(action=edit) on captionable messages failed and left the UI inconsistent.", + "solution_summary": "editMessageTelegram now detects the specific 400 error via NO_TEXT_IN_MESSAGE_RE and retries with editMessageCaption for media messages; buttons-only updates call editMessageReplyMarkup directly; MESSAGE_NOT_MODIFIED is treated as success and HTML parse fallback preserved. Six unit tests were added covering retry, buttons-only path, error propagation, and MESSAGE_NOT_MODIFIED handling.", + "maintainer_signal_summary": "Backward-compatible change limited to src/telegram/send.ts, no public API changes; introduces calls to editMessageCaption and editMessageReplyMarkup and a regex to match Telegram's \"no text\" error; includes tests and can be reverted by restoring the previous send.ts. Risk: regex may fail if Telegram changes error wording—unmatched errors will continue to propagate as before.", + "dedupe_summary": "Telegram editMessageText fails for captionable media (photo, video, audio, document, animation) with 400 'there is no text in the message to edit'; fix adds NO_TEXT_IN_MESSAGE_RE to catch that 400 and retry with editMessageCaption (and uses editMessageReplyMarkup for button-only edits), plus tests covering the retry and MESSAGE_NOT_MODIFIED handling." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Clear, concise technical summary naming the exact failure (editMessageText 400 for captionable media) and the precise fix (catch specific 400, retry with editMessageCaption; use editMessageReplyMarkup for button-only edits), so it will cluster correctly with similar Telegram caption-edit issues." + }, + "usage": { + "input_tokens": 1480, + "output_tokens": 598 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "PR #24962 hardcoded disableBlockStreaming: true for WhatsApp to prevent reasoning leaks, which also inadvertently disabled block streaming entirely so messages were batched and sent only when the agent finished, ignoring per-account or global blockStreaming settings.", + "solution_summary": "Restore config-driven behavior: deliver callback now only suppresses tool payloads (not all non-final payloads) and disableBlockStreaming is derived from channels.whatsapp.blockStreaming falling back to agents.defaults.blockStreamingDefault; upstream shouldSuppressReasoningPayload continues to filter reasoning content.", + "maintainer_signal_summary": "All inbound contract tests pass and live WhatsApp verification shows progressive delivery at text_end when configured; reasoning/tool payload suppression remains intact and defaults unchanged; reviewers should watch for any regression in shouldSuppressReasoningPayload.", + "dedupe_summary": "WhatsApp block streaming was disabled by a hardcoded disableBlockStreaming:true in PR #24962; this PR makes block streaming respect per-account channels.whatsapp.blockStreaming and agents.defaults.blockStreamingDefault and narrows suppression in the deliver callback to tool payloads while relying on shouldSuppressReasoningPayload upstream." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core change—removing the hardcoded disable, restoring per-account/global config-driven WhatsApp block streaming, and narrowing suppression to tool payloads while noting upstream reasoning filtering." + }, + "usage": { + "input_tokens": 1472, + "output_tokens": 446 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Sending videos above the old 5MB media limit caused the Telegram gateway to hang indefinitely because undici's Agent.close() deadlocked when the HTTP response body was left unconsumed, making the entire Telegram channel unresponsive until restart.", + "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher to avoid Agent.close deadlock (fallback to destroy after a 3s timeout), added timeouts and retry logic to getFile and media downloads, made media download failures non-fatal so captions/text still forward, and extended UI metadata stripping; tests updated.", + "maintainer_signal_summary": "Default media limit changed (5MB→20MB) but remains configurable via channels.telegram.mediaMaxMb; key files touched: src/infra/net/ssrf.ts and src/telegram/bot.ts; monitor closeDispatcher 3s timeout logs and media download timeouts, and revert limit via config or revert commits if needed.", + "dedupe_summary": "Telegram gateway hung on video messages when unconsumed response bodies caused undici Agent.close deadlock; fix increases mediaMaxMb to 20, adds timeouts/retries to getFile/fetchRemoteMedia, makes download errors non-fatal, and implements a closeDispatcher fallback (destroy after 3s)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core bug (undici Agent.close deadlock on unconsumed response bodies causing gateway hangs) and the key fixes (increase mediaMaxMb, timeouts/retries, non-fatal download errors, closeDispatcher fallback), with no boilerplate noise." + }, + "usage": { + "input_tokens": 1568, + "output_tokens": 626 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "Telegram inbound body fallback always used the hardcoded placeholder \"\" for non-audio media when no caption/text was present, causing documents (PDF/DOCX) to be misidentified to the agent as images and leading to incorrect agent responses.", + "solution_summary": "Changed resolveTelegramInboundBody to use the already-computed, type-aware placeholder variable (e.g., , ) instead of hardcoded , and added unit tests in extensions/telegram/src/bot/helpers.test.ts and extensions/telegram/src/bot.create-telegram-bot.test.ts to cover document and other media types; media download and extraction logic unchanged.", + "maintainer_signal_summary": "PR closes #7116, scoped bugfix to agent-facing placeholder only (no changes to media download/processing), includes unit tests that validate placeholder text and message handling, and notes a small risk that downstream code pattern-matching on could be affected.", + "dedupe_summary": "resolveTelegramInboundBody hardcoded \"\" for non-audio inbound Telegram media, mislabeling documents as images; fix replaces the hardcoded string with the type-aware placeholder so document messages produce \"\", plus unit tests to lock behavior." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary identifying the function, the mislabeling bug, and the fix (type-aware placeholder) with minimal noise, so it will cluster with similar Telegram media placeholder issues." + }, + "usage": { + "input_tokens": 1395, + "output_tokens": 544 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "MEDIA_MAX_BYTES (5 MB) was hardcoded in src/media/store.ts so users could not override it, causing files larger than 5 MB to silently fail to stage into sandboxed agents and preventing processing of common large files (PDFs, presentations, high-res images).", + "solution_summary": "Adds new media.maxBytes config field (integer, optional, default 5 MB, max 500 MB) and replaces the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(); the gateway reads the config at boot, error messages report the configured limit, and tests were updated and pass.", + "maintainer_signal_summary": "All 56 tests pass and bot review threads were addressed; reviewers should verify backward compatibility (deprecated exported constant retained), schema enforcement of the 500 MB cap, and that per-channel download limits remain untouched; beware disk-usage risk if users set very large limits.", + "dedupe_summary": "Make MEDIA_MAX_BYTES configurable via media.maxBytes (default 5 MB, capped at 500 MB), replacing hardcoded 5MB in src/media/store.ts with getMediaMaxBytes()/setMediaMaxBytes(), applied at gateway boot, and update error messages to show the configured MB value; tests updated and passing." + }, + "judge": { + "score": 4, + "has_boilerplate": true, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise and specific about the core change (making MEDIA_MAX_BYTES configurable, where it's applied, replacement of hardcoded constant, default and cap), but includes a minor testing note that is unnecessary for deduplication." + }, + "usage": { + "input_tokens": 1516, + "output_tokens": 873 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "When Telegram media download fails (due to exceeding mediaMaxMb default 5MB or Telegram Bot API getFile 20MB limit, or other API errors), the handler returned early and silently dropped the entire message so the agent received no ack, no visibility, and the user got no useful feedback.", + "solution_summary": "On download failure, extract file metadata (name, size, type, dimensions) from the Telegram message object and inject a metadata stub into msg.text describing the file and why it couldn't be downloaded (e.g. exceeds limit or download failed), then let the message continue through the normal pipeline so the agent can handle/respond.", + "maintainer_signal_summary": "This is a bug fix to the Telegram integration (src/telegram/bot-handlers.ts) and is backward-compatible; previous reply-to-user notifications were removed so the agent decides responses. Verified by code/path inspection (not live runtime), safe to revert the single commit if needed, and the change only mutates msg.text in error paths previously dropping messages.", + "dedupe_summary": "Telegram handler previously returned early on media download failures and dropped messages; now it builds a file metadata stub from the Telegram message object and injects it into msg.text so files exceeding mediaMaxMb (default 5MB) or Telegram getFile limits (20MB) and other download errors are forwarded to the agent with an explanatory warning." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that clearly states the bug (Telegram handler dropped messages on media download failure), the implemented fix (builds file metadata stub and injects it into msg.text), and the relevant conditions (mediaMaxMb and Telegram getFile limits), so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 1358, + "output_tokens": 660 + } + }, + { + "thread_id": 5796, + "number": 24117, + "kind": "pull_request", + "title": "feat(cli): add --session-key targeting for openclaw agent", + "summary": { + "problem_summary": "The openclaw agent CLI lacked a way to target stable session keys, forcing callers to rely on rotating sessionId UUIDs, sender-derived --to, or falling back to the agent main key, causing callback routing ambiguity for automation flows.", + "solution_summary": "Adds a new --session-key flag to openclaw agent, surfaces sessionKey through AgentCliOpts into resolveSessionKeyForRequest, maintains precedence (sessionKey > sessionId > --to > --agent), updates help/validation text, and adds tests for CLI parsing and gateway forwarding.", + "maintainer_signal_summary": "Small, low-risk change (label size: S) with tests passing and a stated confidence score of 5/5; changes touch register.agent.ts, agent-via-gateway.ts, AgentCliOpts, and corresponding tests; backward-compatible and opt-in.", + "dedupe_summary": "Introduce --session-key to openclaw agent CLI and forward sessionKey into resolveSessionKeyForRequest so callers can target stable session keys (e.g., agent:demo:slack:channel:c0123456789) instead of UUID sessionId; updates AgentCliOpts, register.agent.ts, agent-via-gateway.ts, help text, and adds unit tests." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Succinctly describes the new --session-key CLI flag, its purpose (target stable session keys vs UUID sessionId), and the key code/files changed, making it a strong signal for deduplication." + }, + "usage": { + "input_tokens": 1330, + "output_tokens": 512 + } + }, + { + "thread_id": 766, + "number": 11611, + "kind": "pull_request", + "title": "feat: separate group-level allowlist from sender-level command authorization", + "summary": { + "problem_summary": "When channels.whatsapp is configured with groupPolicy: \"allowlist\" the current implementation uses groupAllowFrom (sender phone numbers) to gate all group interactions, preventing admins from allowlisting specific group JIDs, allowing anyone to chat in approved groups, or keeping slash commands restricted to owners while opening group chat to all participants.", + "solution_summary": "If groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access control now gates by group JID membership in that mapping (only listed groups accepted) while leaving groupAllowFrom to authorize slash commands; if no groups mapping is present, the prior sender-based allowlist behavior is preserved.", + "maintainer_signal_summary": "Code changes touch src/web/inbound/access-control.ts (integrating resolveChannelGroupPolicy), update types JSDoc, and add a Vitest suite with four tests covering allowlist-by-group, blocking unapproved groups, legacy fallback, and command authorization separation; depends on PR #3326 and notes one stale inline comment to fix. Tests cover new branches and the change is described as localized and safe to merge.", + "dedupe_summary": "WhatsApp: change groupPolicy: \"allowlist\" behavior so that when channels.whatsapp.groups mapping exists inbound group access is gated by group JID membership (allow any sender in approved groups), while groupAllowFrom remains the gate for slash commands; falls back to sender-based filtering when no groups mapping is present." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Accurately and concisely describes the core behavioral change (group JID-based gating when groups mapping exists, sender-based fallback, and command authorization via groupAllowFrom) using specific config terms relevant for clustering." + }, + "usage": { + "input_tokens": 1380, + "output_tokens": 784 + } + }, + { + "thread_id": 17924, + "number": 46425, + "kind": "pull_request", + "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", + "summary": { + "problem_summary": "Configured Telegram mediaMaxMb is ignored when delivering final reply messages containing media; loadWebMedia() in the reply delivery path always falls back to the default 100 MB cap so lower account limits aren’t enforced.", + "solution_summary": "Threaded mediaMaxBytes through the Telegram outbound reply chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia receives the configured maxBytes for replies; added unit tests to verify behavior and left direct-send logic unchanged.", + "maintainer_signal_summary": "Two new unit tests added and all related test suites pass (108 tests across delivery, dispatch, native-commands, bot-message); change is small, backward-compatible, author addressed review comments and marked the PR ready with minimal risk.", + "dedupe_summary": "Outbound Telegram replies ignored telegramAccounts[].mediaMaxMb (loadWebMedia fell back to 100 MB); this PR threads mediaMaxBytes through bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts and bot-native-commands.ts so replies enforce the configured media size limit, with unit tests verifying maxBytes is passed or omitted." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the bug and the code paths changed (including filenames and config key) that would match duplicates, but it includes a minor testing-detail noise about unit tests that isn't necessary for deduplication." + }, + "usage": { + "input_tokens": 1381, + "output_tokens": 811 + } + }, + { + "thread_id": 5712, + "number": 37050, + "kind": "pull_request", + "title": "fix(telegram): add HTML parse fallback for media captions", + "summary": { + "problem_summary": "Sending media messages (photo, document, video, animation, audio, voice, video note) failed with a 400 \"can't parse entities\" error when captions contained invalid HTML entities because the media send path lacked the HTML-parse fallback that text messages use.", + "solution_summary": "Wrapped the media send callback in withTelegramHtmlParseFallback so on an HTML parse error the send is retried without parse_mode and with the raw caption; applied to all media types and covered by a new unit test that simulates a parse error then a successful retry.", + "maintainer_signal_summary": "Added a unit test 'retries media caption as plain text on HTML parse error'; all 52 tests pass. Change is backward-compatible, limited to send.ts media send callback, and can be reverted by restoring that function; risk is only visual differences in caption formatting when fallback occurs.", + "dedupe_summary": "Telegram media send (sendPhoto/sendDocument/sendVideo/sendAnimation/sendAudio/sendVoice/video note) had no HTML-parse fallback, causing 400 parse-entity errors for captions with invalid HTML; this PR adds withTelegramHtmlParseFallback to retry without parse_mode and send the raw caption." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the bug (media captions lacked HTML-parse fallback causing 400 errors) and the fix (wrap sends with withTelegramHtmlParseFallback to retry without parse_mode), with no boilerplate or noise." + }, + "usage": { + "input_tokens": 1255, + "output_tokens": 492 + } + }, + { + "thread_id": 21769, + "number": 55280, + "kind": "pull_request", + "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", + "summary": { + "problem_summary": "WhatsApp had block streaming effectively disabled (disableBlockStreaming hardcoded true), causing intermediate text blocks to be batched and sometimes resulting in a delivery race where neither blocks nor the final reply reached the user. A ternary bug also caused configured blockStreaming to be ignored (undefined vs false).", + "solution_summary": "Introduce channels.whatsapp.blockStreaming (per-account/global), add sensible coalescing defaults, and change the dispatcher to sendBlockReplyAsync which awaits confirmed delivery before marking sentContentKeys. Update process-message to read the account config and fix the ternary, update dispatch to await async block delivery, and add/update tests for both streaming paths.", + "maintainer_signal_summary": "Tests updated and new cases added; manual verification shows streaming off yields a single final reply and streaming on delivers intermediate blocks; PR targets root-cause fix of the silent-turn bug rather than the prior revert workaround. Labels: channel: whatsapp-web, channel: feishu, size: S.", + "dedupe_summary": "Block streaming was effectively disabled for WhatsApp via a hardcoded disableBlockStreaming=true and a ternary bug ignored configured blockStreaming, causing a race: optimistic enqueue of block replies marked sentContentKeys before actual delivery, suppressing final replies; fix adds channels.whatsapp.blockStreaming config, blockStreamingCoalesceDefaults, and sendBlockReplyAsync which awaits delivery before marking sentContentKeys." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the root cause (hardcoded disableBlockStreaming and ternary bug), the delivery race (sentContentKeys set before delivery), and the concrete fixes (config option, coalescing defaults, and awaiting delivery via sendBlockReplyAsync)." + }, + "usage": { + "input_tokens": 1168, + "output_tokens": 588 + } + }, + { + "thread_id": 8098, + "number": 25543, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked a group-level allowlist: only sender-level groupAllowFrom was supported, so specific Signal groups couldn't be explicitly allowed or authorized to run control commands independent of sender checks.", + "solution_summary": "Adds channels.signal.groups. config plus SignalGroupConfig type and Zod schema, wires resolveChannelGroupPolicy into the Signal event handler to bypass sender-level groupAllowFrom for listed groups, and integrates group-level allow into command authorizers; docs updated with examples.", + "maintainer_signal_summary": "Addresses Greptile review by incorporating groupExplicitlyAllowed into commandGate authorizers, preserves hasGroupAllowFrom backwards-compat behavior, passes TypeScript checks and was manually tested; safe to merge with minimal risk.", + "dedupe_summary": "Introduce channels.signal.groups. allowlist that bypasses sender-level groupAllowFrom and authorizes listed Signal groups for message delivery and control commands by wiring resolveChannelGroupPolicy into the Signal event handler and adding SignalGroupConfig and Zod schema." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core change—adds a Signal group-level allowlist that bypasses sender checks, integrates with the event handler for delivery/command authorization, and adds config/types/schema—no boilerplate or irrelevant details." + }, + "usage": { + "input_tokens": 1110, + "output_tokens": 397 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel had no group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" with group IDs in groupAllowFrom, group messages were silently dropped because the sender-level filter compared sender IDs to group IDs and always failed.", + "solution_summary": "Added a groups config to SignalAccountSchemaBase (requireMention, tools, toolsBySender) and updated Signal's event handler to call resolveChannelGroupPolicy for group-level checks before falling back to sender-level groupAllowFrom; added four tests and verified full test suite passes.", + "maintainer_signal_summary": "Fixes #25540; small Signal-channel change with 4 new tests; all 624 config tests, 6 Signal monitor tests, and 17 group-policy tests pass. Labeled channel: signal, size: S. PR was AI-assisted and reviewed by @JamesPeck.", + "dedupe_summary": "Signal lacked group-level allowlisting (channels.signal.groups), so groupPolicy:\"allowlist\" with group IDs in groupAllowFrom failed because code compared sender phone/UUID to group IDs; fix adds groups schema and calls resolveChannelGroupPolicy in Signal access checks." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the bug and fix: missing group-level allowlist in Signal caused sender-vs-group ID mismatch and was resolved by adding groups schema and using resolveChannelGroupPolicy, which is ideal for deduplication." + }, + "usage": { + "input_tokens": 910, + "output_tokens": 513 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "WhatsApp messages were being coalesced into a single final payload because process-message.ts had disableBlockStreaming hardcoded true, so intermediate agent blocks (tool calls/sleeps) were not delivered as separate WhatsApp messages.", + "solution_summary": "Make WhatsApp respect the channels.whatsapp.blockStreaming config by adding resolveWhatsAppBlockStreamingEnabled(), replacing the hardcoded flag, registering blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in dock.ts, and fixing delivery logic so intermediate blocks count as delivered. Tests updated to cover enabled/default/disabled behaviors.", + "maintainer_signal_summary": "Change is backwards-compatible by defaulting to the previous behavior when blockStreaming is omitted/false, aligns WhatsApp with Telegram/Line behavior, includes a small test update (process-message.inbound-contract.test.ts) and capability registration in dock.ts; labeled channel: whatsapp-web and size: S.", + "dedupe_summary": "process-message.ts hardcoded disableBlockStreaming:true causing WhatsApp to batch intermediate blocks; PR switches to config-driven channels.whatsapp.blockStreaming via resolveWhatsAppBlockStreamingEnabled(), fixes queuedFinal-only delivery counting so intermediate payloads mark replies as delivered, registers blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in dock.ts, and updates inbound contract tests." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core change (removing hardcoded disableBlockStreaming for WhatsApp, making it config-driven, fixing delivery-count logic, and registering capability) with relevant file names and constants, no boilerplate." + }, + "usage": { + "input_tokens": 660, + "output_tokens": 555 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v7-two-pass.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v7-two-pass.json new file mode 100644 index 0000000..164be15 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v7-two-pass.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v7-two-pass", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "You will summarize a GitHub issue or pull request in two mental passes:\n\nPASS 1 — STRIP: Mentally remove all of the following before summarizing:\n- Template sections: headers like \"### Bug type\", \"### Steps to reproduce\", \"### Checklist\", \"## Testing\"\n- Checkboxes: \"- [x] Tests added\", \"- [ ] Docs updated\"\n- Environment details: OS versions, browser versions, Node versions\n- Reproduction steps: numbered step lists\n- Testing instructions: \"Run `npm test`\", \"Deploy to staging\"\n- Cross-references: \"Fixes #1234\", \"Related to #5678\"\n- Formatting: horizontal rules, badges, images unless they show the bug\n\nPASS 2 — EXTRACT: From what remains, identify:\n- What specific component or API is affected?\n- What exactly goes wrong or what change is being made?\n- What is the user-facing impact?\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nAll fields: plain text, no markdown, 1-3 sentences.\ndedupe_summary: the essential topic that would match other reports of the same issue. Be specific enough to distinguish from similar-but-different issues.", + "timestamp": "2026-03-30T17:57:44.188Z", + "aggregate": { + "avg_score": 4.93, + "avg_signal_density": 4.93, + "boilerplate_count": 1, + "boilerplate_pct": 3, + "would_cluster_correctly_pct": 100, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 48503, + "total_output_tokens": 27223 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "channels.discord.proxy only applies to the Discord WebSocket gateway; startup REST API calls (deploy commands, fetch bot identity) and the guild resolve path do not use the configured HTTP proxy and time out, leaving guild channels unresolved and the bot non-responsive in guilds when a proxy is required.", + "solution_summary": "Apply the configured proxy to undici's dispatcher for all Discord REST calls (including guild resolve) during provider startup or add a global proxy config that sets a ProxyAgent/setGlobalDispatcher; ensure guild resolution uses the proxied HTTP path and add retry logic.", + "maintainer_signal_summary": "Regression: previously worked but now fails in proxied environments; reproducible when an HTTP proxy is required. Logs show WebSocket proxied but REST calls fail with \"fetch failed\" and \"channels unresolved\"; a manual undici ProxyAgent bootstrap fixes REST but not guild resolve, indicating a separate code path. High priority because guild channels are unusable behind proxies.", + "dedupe_summary": "Discord REST API requests and guild resolution bypass channels.discord.proxy, causing REST timeouts and unresolved guild channels when an HTTP proxy is required." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific description of the bug (REST calls/guild resolution bypassing channels.discord.proxy and causing timeouts/unresolved channels) with no boilerplate, so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 2348, + "output_tokens": 724 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "Discord outbound REST sends (message sending) fail when channels.discord.proxy is set because Carbon's RequestClient uses fetch without proxy/agent support. Gateway/WebSocket connections work via the proxy, but REST calls error with \"fetch failed\", preventing bots from sending messages in proxy-required networks.", + "solution_summary": "Make the Carbon RequestClient honor channels.discord.proxy by wiring a proxy-capable HTTP/SOCKS agent into fetch (or using a proxy-aware HTTP client), ensuring all Discord REST endpoints, attachments, and media downloads route through the configured proxy; add tests and config documentation.", + "maintainer_signal_summary": "High-priority for users in proxy-restricted regions (tested in China); reproduction provided (channels.discord.proxy: \"http://127.0.0.1:7890/\") and logs show \"rest proxy enabled\" but fetch ignores the proxy. Fix requires adding proxy support to RequestClient/fetch or using env-based proxying.", + "dedupe_summary": "channels.discord.proxy not applied to Discord REST API (message send) because Carbon RequestClient/fetch lacks proxy support, causing send failures in proxy-required environments." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary stating that channels.discord.proxy isn't applied to REST sends because the Carbon RequestClient/fetch lacks proxy support, which directly explains the failure in proxy-required environments." + }, + "usage": { + "input_tokens": 1030, + "output_tokens": 746 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "When OpenClaw's Discord provider is configured with an HTTP proxy, the gateway/WebSocket login succeeds but some startup REST calls—specifically native command deployment (PUT /applications/.../commands) and bot identity fetch—consistently fail with \"fetch failed\", leaving the provider half-functional and triggering periodic stale-socket restarts.", + "solution_summary": "Ensure all Discord REST requests use the same proxy-aware fetch/dispatcher as the gateway/login by unifying transport code paths; apply proxy handling to native command deployment and bot identity fetch, add logging/tests to verify proxy usage and prevent mixed proxy/direct requests.", + "maintainer_signal_summary": "Investigate and reproduce with the reporter's proxy config, inspect differences between gateway and REST request code paths, patch the REST request implementation to use the proxy-aware dispatcher, add unit/integration tests and improved logging, and request additional logs/config if needed.", + "dedupe_summary": "Discord provider startup REST requests (native command deployment and bot identity fetch) fail with \"fetch failed\" behind an HTTP proxy while gateway login succeeds." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it names the failing Discord provider REST calls (native command deployment and bot identity fetch), the error ('fetch failed'), and that it occurs behind an HTTP proxy while gateway login succeeds, with no boilerplate noise." + }, + "usage": { + "input_tokens": 1176, + "output_tokens": 818 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "OpenClaw's Discord channel integration (discord.js REST client/gateway) logs that an HTTP proxy is enabled but HTTP REST calls to Discord fail with \"fetch failed\" and the WebSocket gateway never establishes; as a result the bot cannot deploy commands or fetch identity and the Discord channel is unusable.", + "solution_summary": "Ensure the configured HTTP proxy is actually passed to the underlying discord.js HTTP/fetch implementation and gateway connection (e.g., attach a proxy agent or configure fetch/global agent), so REST requests and the gateway use the proxy and succeed under WSL2/HTTP-proxy environments.", + "maintainer_signal_summary": "Config at ~/.openclaw/openclaw.json shows channels.discord.proxy and OpenClaw logs indicate proxy enabled, while curl through the same proxy succeeds; errors shown: deploy-rest:put:error ... error=fetch failed and failed to fetch bot identity; reproducible in WSL2, high severity, workaround is TUN-mode or alternate channel.", + "dedupe_summary": "Discord channel proxy config logged as enabled but discord.js REST/gateway calls fail with \"fetch failed\" under HTTP proxy (WSL2), causing Discord integration to be nonfunctional." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states that the Discord proxy is detected but discord.js REST/gateway calls fail with 'fetch failed' (WSL2), which accurately captures the core issue for deduplication." + }, + "usage": { + "input_tokens": 1013, + "output_tokens": 614 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "Discord gateway's REST API calls (sending messages) ignore the configured proxy (channels.discord.proxy and HTTP(S)_PROXY), causing 'TypeError: fetch failed' while the Discord WebSocket connection and curl via the proxy work correctly.", + "solution_summary": "Make Discord REST HTTP requests go through the same proxy-aware SSRF guard/fetch path used for web_search/web_fetch so they honor channels.discord.proxy and environment proxy variables.", + "maintainer_signal_summary": "Reproducible by configuring channels.discord.proxy or HTTP(S)_PROXY and sending a message; logs show 'discord final reply failed: TypeError: fetch failed'. The 2026.2.26 fix added a proxy-aware path for web tools but omitted Discord REST; Telegram and Discord WebSocket already honor the proxy.", + "dedupe_summary": "Discord REST API (message send) ignores configured proxy settings and fails with 'fetch failed' while WebSocket and other channels use the proxy correctly." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (Discord REST message sends ignore proxy and error with 'fetch failed', while WebSocket works), with no template noise and good keywords for clustering." + }, + "usage": { + "input_tokens": 1036, + "output_tokens": 707 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the CLI command 'openclaw agent --channel discord --to ...' delivers to agent:*:main instead of the live Discord channel/thread session because the session resolver does not search by delivery context and Discord targets are not normalized.", + "solution_summary": "Manual patch to compiled dist files added session lookup by delivery context (channel+to+accountId), normalized Discord targets, propagated channel and replyAccount/accountId through the command path, and corrected resolver logic to use buildAgentMainSessionKey; post-patch validation shows CLI now routes to the correct channel/thread sessions.", + "maintainer_signal_summary": "High regression risk: changes were made directly to compiled dist files and will be lost on update; an authoritative source-level fix and tests are required, plus a post-update revalidation script to avoid regressions.", + "dedupe_summary": "CLI 'openclaw agent --channel discord --to' misroutes Discord deliveries to agent:*:main because the session resolver ignores delivery context (channel+to+accountId) and lacks Discord target normalization." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states the CLI misroutes to agent:*:main and identifies the resolver ignoring delivery context and missing Discord target normalization, which is the core issue for deduping." + }, + "usage": { + "input_tokens": 963, + "output_tokens": 697 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions in OpenClaw Discord integration: (1) Discord-thread messages repeatedly had volatile metadata re-injected every turn (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply), causing progressive context bloat and performance degradation; (2) CLI agent commands (openclaw agent --channel discord --to ...) resolved to the default agent:*:main session instead of the live thread-bound session, so commands landed in the wrong session.", + "solution_summary": "Patched compiled runtime bundles: prevented reinjection of volatile Discord thread metadata in pi-embedded; added delivery-target-based session resolution and Discord target normalization in gateway-cli; added delivery-context lookup in the agent command resolver (compact); passed channel and replyAccount/accountId to the resolver and replaced incorrect resolveAgentMainSessionKey usage with buildAgentMainSessionKey(...).", + "maintainer_signal_summary": "Changes are manual edits to compiled files in openclaw/dist and will be lost on update/reinstall, so maintainers should apply the patch or an automated re-application script after updates and restart openclaw-gateway.service; verification shows writer/reasoning sessions producing real toolCall output, threads no longer bloat, and CLI routing now uses the actual Discord target.", + "dedupe_summary": "Discord threads: repeated injection of volatile metadata each turn causing context bloat and CLI agent commands resolving to the main session instead of the thread-bound session due to resolver not using delivery context." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures both core regressions (per-turn re-injection of volatile Discord thread metadata causing context bloat, and CLI commands resolving to the main session because the resolver ignored delivery context) without boilerplate." + }, + "usage": { + "input_tokens": 882, + "output_tokens": 839 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "The sandbox media staging cap is hardcoded as MEDIA_MAX_BYTES = 5 * 1024 * 1024 in dist/store-*.js, causing files larger than 5 MB (PDFs, PPTX, images) to be silently rejected or not copied into sandboxed agents.", + "solution_summary": "Make the staging limit configurable (e.g. support tools.media.maxBytes in openclaw.json) and change the distributed code to read that config instead of using a hardcoded MEDIA_MAX_BYTES constant; preserve or map existing per-type keys as appropriate.", + "maintainer_signal_summary": "This is a regression present since at least v2026.3.7 and still in v2026.3.8; maintainers should remove the hardcoded constant from the distributed build and implement config-driven limits (and provide a patch or release to avoid manual dist edits).", + "dedupe_summary": "Sandbox media staging cap hardcoded to 5MB (MEDIA_MAX_BYTES) preventing files >5MB from being staged; needs a tools.media.maxBytes config option." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focuses on the core problem (hardcoded 5MB MEDIA_MAX_BYTES blocking larger files) and the needed config key (tools.media.maxBytes), with no template noise." + }, + "usage": { + "input_tokens": 929, + "output_tokens": 592 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "OpenClaw's Discord REST API requests fail with \"fetch failed\" despite logs showing the rest proxy is enabled and the Gateway/WebSocket working. The bot receives messages but cannot send replies; direct curl requests through the same proxy succeed, indicating only REST requests from OpenClaw are failing.", + "solution_summary": "Ensure the REST proxy path honors the configured proxy (make fetch or the HTTP agent respect per-provider proxy settings) so Discord REST requests are routed through the proxy; alternatively provide a documented workaround (e.g., using a proxy agent or env vars) until the proxy handling is fixed.", + "maintainer_signal_summary": "Logs show \"rest proxy enabled\" and \"gateway proxy enabled\" but native command deploy and REST calls throw TypeError: fetch failed. Telegram and direct curl via the same proxy work, isolating the issue to OpenClaw's REST fetch/proxy handling rather than the proxy or Discord network.", + "dedupe_summary": "Discord REST API requests in OpenClaw fail with \"fetch failed\" even though rest proxy is enabled and curl via the same proxy works (Gateway/WebSocket unaffected)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (Discord REST requests failing with 'fetch failed' in OpenClaw despite rest proxy being enabled and curl/proxy/Gateway working), with no extraneous template or testing details." + }, + "usage": { + "input_tokens": 874, + "output_tokens": 773 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "Discord REST API calls made via @buape/carbon's RequestClient (and other globalThis.fetch usages like memorySearch) ignore the channels.discord.proxy configured in openclaw.json; only the WebSocket gateway uses the proxy, so REST requests bypass the configured HTTP proxy.", + "solution_summary": "Modify @buape/carbon's RequestClient to accept and use an optional dispatcher/proxy configuration and have OpenClaw pass channels.discord.proxy to it so REST fetches route through the configured proxy; as a temporary workaround, preload an EnvHttpProxyAgent, but do not use NODE_OPTIONS=--use-env-proxy.", + "maintainer_signal_summary": "This is a correctness/security issue causing inconsistent proxy handling and potential network leakage; it requires a change in @buape/carbon to surface dispatcher/proxy configuration and a corresponding OpenClaw change to propagate channels.discord.proxy, with a documented workaround and warning about --use-env-proxy.", + "dedupe_summary": "@buape/carbon RequestClient (Discord REST fetch) ignores openclaw.json channels.discord.proxy and calls globalThis.fetch without a proxy-aware dispatcher." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it identifies the exact component (@buape/carbon RequestClient), the config key ignored (channels.discord.proxy in openclaw.json), and the incorrect behavior (using globalThis.fetch without proxy support), so it will cluster with similar proxy-related REST issues." + }, + "usage": { + "input_tokens": 883, + "output_tokens": 722 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "OpenClaw gateway/session handling and Discord channel lanes experience unbounded conversation context growth (writer sessions ~42k tokens, reasoning ~90k) which increases per-turn latency, long turns block lanes causing serialization under load, and typing indicators can remain stuck until TTL in some code paths, degrading performance under sustained load.", + "solution_summary": "Applied conservative configuration changes: reduced concurrency (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents 8→4, maxChildrenPerAgent 8→4), increased channels.discord.eventQueue.listenerTimeout 120000→300000, set agents.defaults.contextTokens to 80000, improved patching script for hash variants, and restarted the gateway; observed no new lane-wait or typing-TTL errors post-restart but restarts do not fully eliminate issues under sustained load.", + "maintainer_signal_summary": "Restarting the gateway mitigates symptoms but is not a full fix; primary remediation is enforcing context token limits, tightening concurrency controls, and increasing event queue timeouts. Recommend making limits adaptive or exposing them via openclaw config so operators can tune behavior without manual JSON edits.", + "dedupe_summary": "Unbounded conversation context token growth leading to lane blocking/serialization and typing-indicator timeouts in OpenClaw gateway (Discord channel) under sustained high load." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core problem (unbounded context growth causing lane blocking/serialization and typing-indicator timeouts in the OpenClaw Discord gateway) without boilerplate or irrelevant details." + }, + "usage": { + "input_tokens": 817, + "output_tokens": 716 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "OpenClaw v2026.3.11’s Discord thread runtime repeatedly reinjects volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context contamination, increased token usage, broken prompt caching and performance degradation in long threads.", + "solution_summary": "Manual patches to compiled dist: stopped per-turn injection of the listed Discord thread fields in dist/pi-embedded-D6PpOsxP.js, added an isNewSession guard and replaced resolveAgentMainSessionKey with buildAgentMainSessionKey in dist/compact-1mmJ_KWL.js; post-patch validation shows repeated context stopped and prompt caching restored.", + "maintainer_signal_summary": "HIGH risk: fixes were applied directly to compiled dist files and will be lost on update; recommend an official source-level fix, add unit/regression tests, and a post-update revalidation script to detect regressions.", + "dedupe_summary": "Discord threads reinjecting volatile metadata each turn (message_id/reply_to_id/timestamp/Thread starter/history) causing context pollution and prompt cache bust in OpenClaw v2026.3.11" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core bug (Discord thread metadata re-injection causing context pollution and cache busting) but includes a version number which is unnecessary for deduplication." + }, + "usage": { + "input_tokens": 829, + "output_tokens": 745 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord thread messages have volatile metadata re-injected on every turn (message_id, reply_to_id, timestamp, Thread starter, and Chat history since last reply), causing context pollution, prompt cache busting, increased token usage and degraded performance in long threads.", + "solution_summary": "A local hotfix modified the compiled runtime (dist/pi-embedded-D6PpOsxP.js) to inject that Discord thread metadata only on the first turn; validation shows thread context no longer repeats, prompt caching is effective, and token usage is reduced. The fix needs to be applied in source to be permanent.", + "maintainer_signal_summary": "High-priority, high-regression-risk issue because the change was applied to compiled files in dist; please implement the fix in source, ship a patch release, and add a post-update revalidation step to avoid losing the change on upgrades. Affected file: dist/pi-embedded-D6PpOsxP.js.", + "dedupe_summary": "Discord thread metadata re-injected every message by dist/pi-embedded-D6PpOsxP.js (message_id, reply_to_id, timestamp, Thread starter, Chat history) causing context pollution, token bloat and prompt cache busting." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and focused summary naming the exact compiled file, the specific Discord metadata fields being re-injected, and the impact (context pollution, token bloat, prompt cache busting), so it will cluster well with similar reports." + }, + "usage": { + "input_tokens": 837, + "output_tokens": 719 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "OpenClaw runtime for Discord threads is suffering two regressions: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) that causes unbounded context growth, and the session resolver falling back to agent:*:main instead of resolving live thread-bound sessions. User impact is progressive performance degradation and latency growth in long threads, and CLI commands resolving to the wrong session.", + "solution_summary": "Applied runtime patches to compiled bundles (dist/reply-BCcP6j4h.js, dist/compact-1mmJ_KWL.js, dist/pi-embedded-Cz5VjpnY.js, dist/gateway-cli-B-E8XzUM.js, dist/register.agent-jS8B5meo.js) to ensure resolver looks up sessions by delivery context (channel + to + accountId) and to stop repeated metadata reinjection; created idempotent re-application script and backups, restarted and validated the gateway.", + "maintainer_signal_summary": "This is a manual, compiled-bundle hotfix that will be lost on updates; maintainers should merge fixes into source, add the recommended smoke/regression tests (context size per turn and latency), and adopt the reapply script until upstream release includes the changes. Gateway restart and verification checks passed.", + "dedupe_summary": "Discord threads: per-turn metadata duplication causing context growth plus session resolver fallback to agent:*:main, leading to wrong CLI session routing and performance/latency regression." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core bug (per-turn metadata duplication causing context growth) and the related session resolver fallback to agent:*:main that causes misrouted CLI sessions and performance/latency regressions." + }, + "usage": { + "input_tokens": 813, + "output_tokens": 778 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "OpenClaw's Discord integration fails to send reply messages: incoming messages are received and model responses generated, but sending the final reply errors with \"final reply failed: TypeError: fetch failed\" despite the configured Discord HTTP proxy being enabled.", + "solution_summary": "Investigate and fix regression where the Discord REST send path is not using the configured HTTP proxy (or the fetch call changed), causing fetch to fail; ensure the proxy agent/environment is applied to all Discord REST requests and add a regression test.", + "maintainer_signal_summary": "Regression in 2026.3.2 on Node.js 22.22.0; logs show \"discord: rest proxy enabled\", \"discord: gateway proxy enabled\", \"logged in to discord\" followed by \"final reply failed: TypeError: fetch failed\". curl through the same proxy works, indicating proxy reachable but not applied to the final reply path. Reproducible with enabled discord channel proxy in openclaw.json for both DM and group chat.", + "dedupe_summary": "Discord REST send failing with \"final reply failed: TypeError: fetch failed\" even though Discord proxy is configured and enabled (regression in applying proxy to reply/fetch path)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: states the failing Discord REST send with the exact error, notes the proxy is configured/enabled, and identifies it as a regression in applying the proxy to the reply/fetch path, which is ideal for deduplication." + }, + "usage": { + "input_tokens": 753, + "output_tokens": 684 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "The CLI command openclaw agent --channel discord --to resolves the wrong session and sends messages to agent:*:main instead of the live Discord channel/thread session because the compiled session resolver does not look up sessions by delivery context and Discord targets are not normalized.", + "solution_summary": "A hotfix was applied to dist/gateway-cli-BjsM6fWb.js to normalize Discord targets (e.g., 148... → channel:148...), perform session lookup by delivery context (channel + to + accountId) and use that session before falling back to agent:*:main, restoring correct routing to live channel/thread sessions.", + "maintainer_signal_summary": "This is a high-regression-risk manual patch on compiled dist/ files and will be lost on reinstall or upgrade; the fix needs to be implemented in source, covered by tests, and coordinated with related issues for a durable solution.", + "dedupe_summary": "CLI agent --channel discord --to routes to agent:*:main due to session resolver missing delivery-context lookup and lacking Discord target normalization in dist/gateway-cli-BjsM6fWb.js." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that names the CLI command, the incorrect resolution (agent:*:main), the root cause (missing delivery-context lookup and Discord target normalization), and the affected file, so it would cluster with similar routing/session resolver bugs." + }, + "usage": { + "input_tokens": 818, + "output_tokens": 485 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the compiled file dist/compact-1mmJ_KWL.js calls a non-existent function resolveAgentMainSessionKey, causing ReferenceError at runtime; the agent command fails and the gateway can crash when processing agent commands.", + "solution_summary": "A manual patch replaced resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...) in dist/compact-1mmJ_KWL.js, which removes the ReferenceError and restores correct session resolver and agent behavior; note this edit is to compiled output and will be lost on reinstall.", + "maintainer_signal_summary": "High-priority regression introduced by the prior session-resolver fix (#3); the real fix must be applied to source/build so compiled artifacts use buildAgentMainSessionKey, and the release pipeline should be corrected to prevent reintroducing the error.", + "dedupe_summary": "Regression in compiled dist/compact-1mmJ_KWL.js using undefined resolveAgentMainSessionKey instead of buildAgentMainSessionKey causing runtime ReferenceError and agent command failures." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies the compiled file, the incorrect function name, and the resulting runtime ReferenceError and agent failures with no boilerplate or noise." + }, + "usage": { + "input_tokens": 789, + "output_tokens": 677 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "The Discord channel plugin's proxy setting (channels.discord.proxy) is only applied to WebSocket connections but not to REST API HTTP calls; as a result the plugin health check returns \"fetch failed\" and message send operations time out or fail when a proxy is required, even though curl via the same proxy works.", + "solution_summary": "Update the Discord plugin's REST HTTP client/health-check implementation to honor channels.discord.proxy (e.g., configure the HTTP fetch/agent or proxy settings used for REST calls), ensure both REST and WebSocket traffic go through the configured proxy, and add tests to cover proxy behavior.", + "maintainer_signal_summary": "Regression: previously worked but now fails (OpenClaw 2026.3.8). Repro: configure channels.discord.proxy and run message send or health check; curl via proxy succeeds but OpenClaw REST calls fail with \"fetch failed\". Likely root cause: REST client not using proxy config; needs a prioritized fix.", + "dedupe_summary": "Discord plugin: channels.discord.proxy not applied to REST API calls, causing health check \"fetch failed\" and message-send failures behind a proxy." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific—identifies the exact config key and that proxy setting isn't applied to Discord REST API calls, causing health-check and send failures, so it will cluster correctly with similar reports." + }, + "usage": { + "input_tokens": 745, + "output_tokens": 534 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the session resolver in the compiled file dist/compact-1mmJ_KWL.js for the agent command does not perform lookup by delivery context (channel+to+accountId), causing sessions to resolve to agent:*:main instead of the live session for the target channel. This produces wrong routing compared to the CLI gateway and agent responses are not associated with the correct channel/thread session.", + "solution_summary": "A manual patch was applied to dist/compact-1mmJ_KWL.js to add delivery-context lookup inside the agent command session resolver, searching for an existing session by channel+to+accountId and only falling back to main when no specific session is found, restoring correct routing and response association.", + "maintainer_signal_summary": "This is a hotfix applied to a compiled dist file and has HIGH regression risk because it will be overwritten on update; maintainers should implement the same delivery-context lookup in source and release a new package so the fix persists.", + "dedupe_summary": "Missing delivery-context (channel+to+accountId) lookup in the agent command session resolver in dist/compact-1mmJ_KWL.js causing fallback to agent:*:main and incorrect routing/response association." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the root cause (missing delivery-context lookup in the agent session resolver in dist/compact-1mmJ_KWL.js) and its direct effect on routing, with no extraneous boilerplate." + }, + "usage": { + "input_tokens": 759, + "output_tokens": 800 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English (settings, buttons, agent/skill pages, channel config, status messages), creating significant usability barriers for non-English users, especially Chinese speakers.", + "solution_summary": "Extract all hardcoded UI strings into a structured i18n layer (e.g., JSON locale files), add a zh-CN locale with translated strings, provide a language selector or auto-detect, and build infrastructure to make adding more languages easy; existing docs/zh-CN show translations and the control-ui bundle contains the hardcoded strings to extract.", + "maintainer_signal_summary": "Request to add i18n support (priority medium-high) and to know whether this is on the roadmap or if maintainers welcome contributions/PRs to implement extraction, zh-CN locale, and a language selector.", + "dedupe_summary": "control-ui hardcoded English UI strings — feature request to implement internationalization and add zh-CN localization with language selection." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core issue (control-ui hardcoded English) and requested change (implement i18n, add zh-CN locale, and language selection) without extraneous boilerplate." + }, + "usage": { + "input_tokens": 680, + "output_tokens": 461 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "WhatsApp group conversations could not apply channel-configured systemPrompt instructions at account or group granularity, so users could not tailor reply behavior per WhatsApp account or per group or use wildcard defaults for groups without their own prompt.", + "solution_summary": "Add systemPrompt support to WhatsApp config schema and resolution: support channels.whatsapp.systemPrompt, accounts..systemPrompt, groups..systemPrompt and accounts..groups..systemPrompt; resolve final prompt by composing account prompt (or root) plus resolved group prompt (specific group then wildcard '*'), with account-level groups fully overriding root groups; includes tests and docs.", + "maintainer_signal_summary": "Change is WhatsApp-specific, backward compatible, touches gateway/orchestration and API/config schema, and does not alter non-WhatsApp channels, hot-reload behavior, or group allowlist/mention-gating semantics; tests and docs included and behavior matches Telegram-style override semantics.", + "dedupe_summary": "WhatsApp channel systemPrompt support for per-account and per-group prompts with wildcard fallback and account-level groups replacing root groups" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core change—adding per-account and per-group WhatsApp systemPrompt support with wildcard fallback and account-level group precedence—without extraneous boilerplate." + }, + "usage": { + "input_tokens": 2456, + "output_tokens": 625 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before agent processing, and WhatsApp group policy helpers resolved without accountId so account-scoped groupAllowFrom/allowFrom could be ignored leading to gate failures.", + "solution_summary": "Limit append processing to WhatsApp group messages, propagate accountId when resolving WhatsApp group policy, and use account-scoped groupAllowFrom/allowFrom as the fallback; DM append history remains skipped.", + "maintainer_signal_summary": "Changes touch src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts, were validated with live repro logs and targeted tests, and introduce no config/schema/dist/debug changes; scope is explicitly limited to WhatsApp groups.", + "dedupe_summary": "WhatsApp group messages via messages.upsert(type=\"append\") dropped before processing and account-scoped channels.whatsapp.accounts. group settings ignored; fix enables group-append processing and uses accountId for policy resolution." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (WhatsApp group append messages dropped and account-scoped group settings ignored) and the fix (enable group-append processing and use accountId for policy resolution) with no extraneous boilerplate." + }, + "usage": { + "input_tokens": 2008, + "output_tokens": 785 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Gateway auth/network code caused Canvas/A2UI to return 401 when accessed via Tailscale Serve: resolveClientIp returned undefined for trusted-proxy requests that had no forwarding headers, and isLocalDirectRequest did not fall back to req.socket.remoteAddress when allowRealIpFallback was enabled; .ts.net host acceptance was also unconditional.", + "solution_summary": "resolveClientIp now returns the socket address as a fallback when no forwarding headers are present; isLocalDirectRequest checks req.socket.remoteAddress when allowRealIpFallback is enabled; .ts.net host acceptance is gated behind allowTailscale and that flag is threaded through callers; docs updated and tests added.", + "maintainer_signal_summary": "Includes new tests (105 passing) and docs; depends on PR #50636 and closes #28472; allowTailscale still defaults to true when tailscale.mode='serve' to preserve existing setups; reviewers should verify trustedProxies and allowRealIpFallback configs and note the limited risk scope (only when XFF/X-Real-IP are absent).", + "dedupe_summary": "401 for Canvas/A2UI behind Tailscale Serve caused by resolveClientIp/isLocalDirectRequest failing to use socket remoteAddress fallback and unconditional acceptance of .ts.net host headers." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and accurate: it states the core failure (401 behind Tailscale Serve) and the technical root causes (missing socket remoteAddress fallback in resolveClientIp/isLocalDirectRequest and .ts.net host handling), with no boilerplate, so it will cluster correctly." + }, + "usage": { + "input_tokens": 1847, + "output_tokens": 821 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "WhatsApp inbound access control (checkInboundAccessControl) computed an isSelfChat heuristic but did not enforce explicit self-chat mode, allowing non-self messages (group messages, DMs from strangers, broadcasts) through because groupPolicy defaulted to \"open\". This violates the intended self-chat isolation and lets messages reach downstream processing (mention gating, debounce, auto-reply).", + "solution_summary": "Added an early-return guard in checkInboundAccessControl that blocks all non-self messages when account.selfChatMode === true, placed before group/DM policy evaluation. The guard checks only the explicit selfChatMode flag (not the heuristic isSelfChat) to avoid blocking teammates and preserves self-messages; tests pass and the change is limited to WhatsApp access control.", + "maintainer_signal_summary": "Reviewer action: review the new early-return guard in src/web/inbound/access-control.ts and confirm behavior for explicit selfChatMode; no changes to group/DM policy logic, monitor.ts, or other channels. Revert by removing the added guard block if needed; note auto-detected self-chat (heuristic) remains unchanged by design.", + "dedupe_summary": "WhatsApp self-chat mode not enforcing explicit selfChatMode: isSelfChat computed but unused, letting non-self group/DM messages pass due to groupPolicy defaulting to \"open\"." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states the core bug (selfChatMode not enforced because isSelfChat is computed but unused, allowing non-self messages through due to default groupPolicy), which is ideal for deduplication." + }, + "usage": { + "input_tokens": 1562, + "output_tokens": 591 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "With groupPolicy \"allowlist\", group messages from senders not in groupAllowFrom are dropped at the access-control layer and never stored as context, so the agent cannot see them in its \"Chat messages since your last reply\" block despite docs expecting non-triggering messages to be kept for context.", + "solution_summary": "Adds an opt-in flag groupContextFromAll (top-level or per-account, default false) so access-control can return storeForContext for non-allowlisted senders; inbound monitor marks those messages as contextOnly, skips read receipts, and records them as pending group history without triggering replies; config types and schema updated.", + "maintainer_signal_summary": "Default false for zero-breaking change, per-account override recommended, locally tested; changes are localized but there is a potential correctness edge around allowlist normalization and bypassing some group-gating updates that could cause subtle misclassification or state inconsistencies.", + "dedupe_summary": "WhatsApp group allowlist dropping non-allowlisted messages instead of preserving them as pending context; opt-in groupContextFromAll to retain such messages as context-only without generating replies." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (non-allowlisted WhatsApp group messages are dropped instead of stored as context) and the fix (opt-in groupContextFromAll to keep them as context-only), with no boilerplate or noise." + }, + "usage": { + "input_tokens": 1579, + "output_tokens": 744 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "The sessions_spawn sub-agent API is changed to accept an optional sessionKey so a spawn can reuse a deterministic session (short keys map to agent:{agentId}:subagent:{key}, fully-qualified keys are validated against targetAgentId, omission falls back to crypto.randomUUID()). User-facing impact: sub-agents can preserve conversation history across spawns, but Greptile review flagged that session keys may bypass allowlisting and other issues exist in the PR.", + "solution_summary": "Implements an optional sessionKey parameter with name-spacing and validation rules, falls back to UUID when omitted, and adds four tests (deterministic key, UUID fallback, fully-qualified passthrough, cross-agent mismatch rejection) to cover behavior.", + "maintainer_signal_summary": "Blocking issues to fix before merge: a test imports a non-existent module (openclaw-tools.js vs clawdbot-tools.js) so tests won’t compile; cross-agent validation has a case-sensitivity bug (embedded agentId not normalized) causing false rejections; inconsistent error handling (throw vs jsonResult) should be unified.", + "dedupe_summary": "Add sessionKey to sessions_spawn to enable deterministic sub-agent sessions with fully-qualified key validation against agentId; includes a case-sensitivity validation bug and broken test import that block merging." + }, + "judge": { + "score": 3, + "has_boilerplate": true, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Summary correctly highlights the new sessionKey and validation behavior (good for clustering) but injects noisy PR-status/testing details (case-sensitivity validation bug, broken test import) that are irrelevant for deduplication." + }, + "usage": { + "input_tokens": 1632, + "output_tokens": 884 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Telegram adapter: editing messages that contain media (photo, video, audio, document, animation) previously called editMessageText and Telegram returned 400 'there is no text in the message to edit', causing message(action=edit) calls to fail and leave the UI inconsistent.", + "solution_summary": "Updated editMessageTelegram in src/telegram/send.ts to detect the specific 400 error (NO_TEXT_IN_MESSAGE_RE), retry with editMessageCaption for captionable media, and use editMessageReplyMarkup directly for buttons-only updates; MESSAGE_NOT_MODIFIED is treated as success and six unit tests were added.", + "maintainer_signal_summary": "This is a backward-compatible bugfix limited to the Telegram adapter with six new tests; risk is limited to the regex not matching future Telegram error text — failure mode is graceful and the change can be reverted by restoring src/telegram/send.ts; no new permissions or interface changes.", + "dedupe_summary": "Telegram edit failure for captionable media messages due to using editMessageText (400 'no text in message'); add detection and retry to editMessageCaption." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core bug and fix—wrong use of editMessageText for captionable media causing 400 and retrying with editMessageCaption—no boilerplate or noise." + }, + "usage": { + "input_tokens": 1513, + "output_tokens": 747 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "WhatsApp delivery was hardcoded to disable block streaming (PR #24962) and the deliver callback suppressed too many payloads, causing all messages to be batched and sent only when the agent finished even when blockStreaming was enabled in config.", + "solution_summary": "Restore config-driven behavior: make disableBlockStreaming derive from per-account channels.whatsapp.blockStreaming (falling back to agents.defaults.blockStreamingDefault) and change the deliver callback to only suppress kind === \"tool\" payloads; reasoning suppression remains upstream via shouldSuppressReasoningPayload.", + "maintainer_signal_summary": "Fix verified with unit/integration tests and live WhatsApp testing (progressive delivery at text_end); original reasoning/tool-leak fixes remain intact. Quick mitigation: set channels.whatsapp.blockStreaming: false or remove blockStreamingDefault to revert; risk if upstream reasoning filter is removed.", + "dedupe_summary": "whatsapp integration ignoring blockStreaming config (hardcoded disableBlockStreaming: true) causing messages to be batched instead of streamed" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific description of the core bug—WhatsApp integration hardcoded disableBlockStreaming:true causing messages to be batched instead of honoring blockStreaming config—so it will cluster with similar fixes." + }, + "usage": { + "input_tokens": 1505, + "output_tokens": 645 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Sending video or large media via the Telegram channel could cause the gateway to hang and become unresponsive. getFile could succeed but fetchRemoteMedia enforced the old 5MB limit, triggering a content-length error and leaving an unconsumed response body that caused Agent.close()/closeDispatcher to deadlock and drop messages.", + "solution_summary": "Increased default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher to enforce a 3s timeout with fallback to destroy to avoid undici Agent deadlock, added timeouts and retry logic to getFile/file downloads, made media-download failures non-fatal so captions/text still forward, and extended stripInboundMetadata; tests updated and passing.", + "maintainer_signal_summary": "Backward-compatible change (configurable mediaMaxMb remains), no SSRF policy or new dependencies, relevant code changes in src/infra/net/ssrf.ts and src/telegram/bot.ts, and rollback options include lowering mediaMaxMb or reverting the commit; watch for the 3s cleanup timeout if drains are slow.", + "dedupe_summary": "Telegram gateway hangs due to unconsumed response body / Agent.close deadlock when downloading media exceeding configured mediaMaxMb (default previously 5MB), causing dropped messages and full channel unresponsiveness." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific — identifies the technical root cause (unconsumed response body causing Agent.close deadlock) and the related mediaMaxMb limit that leads to gateway hangs and dropped messages, which will reliably match similar reports." + }, + "usage": { + "input_tokens": 1601, + "output_tokens": 825 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "The Telegram integration's inbound body fallback in resolveTelegramInboundBody used a hardcoded \"\" when no caption/text was present, which mislabels document (PDF/DOCX) and other non-audio media as images, causing agents to behave as if a document wasn't received.", + "solution_summary": "Replace the hardcoded string with the already-computed type-aware placeholder variable so the fallback emits \"\", \"\", etc. as appropriate; unit tests were added to validate placeholder text and document message handling. Media download and file-extraction pipelines were not changed.", + "maintainer_signal_summary": "Unit tests added in extensions/telegram/src/bot/helpers.test.ts and bot.create-telegram-bot.test.ts to lock placeholder behavior and ensure getFile is called for documents; change is small-scope and backward-compatible, with a noted risk that downstream code matching on \"\" may need updating. Closes #7116.", + "dedupe_summary": "Telegram integration fallback uses hardcoded for non-audio media, causing documents to be misidentified instead of using a type-aware placeholder." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, accurate summary of the bug: Telegram fallback used a hardcoded placeholder causing documents to be misidentified instead of a type-aware , with no boilerplate or extraneous details." + }, + "usage": { + "input_tokens": 1428, + "output_tokens": 648 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "The media store (src/media/store.ts) hardcoded MEDIA_MAX_BYTES to 5 MB, so files larger than 5 MB silently failed to stage into sandboxed containers and users/agents could not process common larger files; error messages also referenced the fixed 5MB limit.", + "solution_summary": "Introduced a new optional config field media.maxBytes (capped at 500 MB), replaced the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(), and made the gateway read and apply the configured limit at boot; default remains 5 MB and error messages now reflect the configured limit.", + "maintainer_signal_summary": "Change is backward-compatible (default preserved, deprecated constant still exported), CI/unit tests pass (56 tests), three bot review threads were fixed, and schema enforces the 500 MB cap; touched gateway/orchestration and media store code.", + "dedupe_summary": "Make MEDIA_MAX_BYTES configurable via media.maxBytes to avoid the hardcoded 5 MB staging limit that prevents files >5 MB from being stored in sandboxed agents." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that names the constant and config field, describes the change and its impact (files >5 MB failing to stage), and contains no template noise." + }, + "usage": { + "input_tokens": 1549, + "output_tokens": 648 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "Telegram media download failures (files over mediaMaxMb or exceeding Telegram API limits, or API errors) caused the handler to return early and silently drop the entire message, so the agent received no ack, no message, and any caption/text was lost.", + "solution_summary": "On download failure, the handler now extracts file metadata (name, size, type) from the Telegram message, injects a metadata stub into msg.text describing the failure, and allows the message to continue through the pipeline so the agent sees and can respond to the file event.", + "maintainer_signal_summary": "Change is limited to the Telegram integration (src/telegram/bot-handlers.ts), is backward compatible, removes prior automatic reply-to-user messages, can be reverted by the single commit, and poses low risk since msg.text is only mutated in error paths; no new permissions or network calls.", + "dedupe_summary": "Telegram bot silently drops messages when media file download fails (size limit or API error); inject file metadata stub and continue processing instead of returning early." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (silent drops on Telegram media download failure) and the implemented fix (inject metadata stub and continue processing) without boilerplate." + }, + "usage": { + "input_tokens": 1391, + "output_tokens": 749 + } + }, + { + "thread_id": 5796, + "number": 24117, + "kind": "pull_request", + "title": "feat(cli): add --session-key targeting for openclaw agent", + "summary": { + "problem_summary": "The openclaw agent CLI has no --session-key option, so CLI-triggered deliveries in multi-channel/multi-agent setups can resolve to the agent main session (agent::main) instead of the intended stable channel session because sessionId is UUID-based and --to or --agent are not always usable in callback/automation contexts.", + "solution_summary": "Adds a first-class --session-key flag to openclaw agent, threads sessionKey through AgentCliOpts into resolveSessionKeyForRequest, leaves resolution semantics intact with precedence (sessionKey > sessionId > to > agent), updates help/validation, and adds tests verifying CLI parsing and gateway forwarding.", + "maintainer_signal_summary": "Touches src/cli/program/register.agent.ts and src/commands/agent-via-gateway.ts, updates validation/help text, and adds two tests (register.agent.test.ts and agent-via-gateway.test.ts). Change is additive, backward-compatible, tested, and safe to merge.", + "dedupe_summary": "Add --session-key flag to openclaw agent CLI to target stable session keys and avoid routing to the agent main session when sessionId/--to are unavailable." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the precise change (new --session-key flag for openclaw agent) and its purpose (target stable session keys to avoid fallback routing), with no template noise." + }, + "usage": { + "input_tokens": 1363, + "output_tokens": 513 + } + }, + { + "thread_id": 766, + "number": 11611, + "kind": "pull_request", + "title": "feat: separate group-level allowlist from sender-level command authorization", + "summary": { + "problem_summary": "WhatsApp inbound access-control currently treats groupPolicy: \"allowlist\" with groupAllowFrom as a sender-level gate, blocking or allowing all group interactions based on sender phone number. This prevents allowing whole groups by JID while still restricting slash commands to specific senders, and allows the bot to be pulled into unapproved/spam groups.", + "solution_summary": "If groupPolicy: \"allowlist\" and a channels.whatsapp.groups mapping exists, the access-control now checks group JIDs against that mapping (allowing anyone in approved groups to chat) while keeping groupAllowFrom as the separate sender-level gate for slash commands; when no groups mapping is present the previous sender-based behavior is preserved.", + "maintainer_signal_summary": "Code changes localized to src/web/inbound/access-control.ts and config JSDoc, plus four new Vitest cases covering approved/blocked groups, legacy fallback, and command authorization; backward-compatible fallback preserved and a small doc/comment update required; depends on #3326.", + "dedupe_summary": "WhatsApp group allowlist should gate by group JID from channels.whatsapp.groups (when groupPolicy: \"allowlist\") instead of filtering all group interactions by sender phone number." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core change—use group JIDs from channels.whatsapp.groups to gate access under groupPolicy: 'allowlist' instead of sender phone filtering—so it will cluster with similar changes." + }, + "usage": { + "input_tokens": 1413, + "output_tokens": 605 + } + }, + { + "thread_id": 17924, + "number": 46425, + "kind": "pull_request", + "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", + "summary": { + "problem_summary": "Telegram outbound reply delivery ignored the configured mediaMaxMb: loadWebMedia in the reply path received no maxBytes and fell back to the 100 MB default, so replies could exceed operator-configured limits.", + "solution_summary": "Threaded mediaMaxBytes through the Telegram outbound reply delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path so loadWebMedia receives the configured maxBytes; added unit tests to verify behavior; direct-send path unchanged.", + "maintainer_signal_summary": "Small, backward-compatible bugfix limited to Telegram integration; two unit tests added and all related test suites (108 tests) pass; change is safe to revert by reverting the commit if needed.", + "dedupe_summary": "Telegram outbound reply media ignores mediaMaxMb because loadWebMedia is called without maxBytes in the reply delivery path." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary identifying the exact bug and root cause (mediaMaxMb ignored because loadWebMedia called without maxBytes in the reply delivery path) with no extraneous boilerplate." + }, + "usage": { + "input_tokens": 1414, + "output_tokens": 580 + } + }, + { + "thread_id": 5712, + "number": 37050, + "kind": "pull_request", + "title": "fix(telegram): add HTML parse fallback for media captions", + "summary": { + "problem_summary": "Telegram media send (photo/document/video/animation/audio/voice/video note) captions lacked the HTML-parse fallback that text messages have, so captions containing invalid HTML entities (e.g. unmatched < or >) caused the Telegram API to return a 400 and the entire media send to fail, breaking delivery for users including code snippets or special characters.", + "solution_summary": "The media send callback in src/telegram/send.ts was wrapped with withTelegramHtmlParseFallback so on an HTML parse error the send is retried without parse_mode and with the raw caption; this applies to all media types and a unit test was added to verify the retry behavior.", + "maintainer_signal_summary": "Change is narrowly scoped to the media send inner callback (sendPhoto/sendDocument/sendVideo/sendAnimation/sendAudio/sendVoice/video note); text message, edit message, thread fallback, and caption-splitting logic are unchanged. Tests added mock sendPhoto to assert first call uses HTML and second call retries without parse_mode; change is backward-compatible and reversible by reverting send.ts.", + "dedupe_summary": "Telegram media captions causing 400 \"can't parse entities\" errors due to invalid HTML entities; add retry that sends caption as plain text (no parse_mode)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: identifies Telegram media caption HTML parse errors causing 400s and the added retry fallback to send captions as plain text, without boilerplate or noise." + }, + "usage": { + "input_tokens": 1288, + "output_tokens": 581 + } + }, + { + "thread_id": 21769, + "number": 55280, + "kind": "pull_request", + "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", + "summary": { + "problem_summary": "WhatsApp had block streaming effectively disabled (hardcoded) and a delivery race in the block-reply pipeline: block replies were optimistically enqueued, the pipeline marked them as sent, and the final reply could be suppressed or nothing delivered if async delivery failed. A ternary bug also caused configured blockStreaming to be ignored (undefined vs false).", + "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming, add sensible coalescing defaults, and introduce dispatcher.sendBlockReplyAsync which is awaited so sentContentKeys are only marked after confirmed delivery; fix the ternary so config actually enables streaming; update tests and mocks. ", + "maintainer_signal_summary": "Tests were updated and new tests added for streaming-on/off paths; mocks adjusted for sendBlockReplyAsync and manual verification done. Change fixes the root-cause delivery race (not just the previous workaround) and keeps blockStreaming opt-in by default. ", + "dedupe_summary": "WhatsApp block-streaming disabled/hardcoded and delivery race where optimistic enqueue suppressed final replies; needs config and async-confirmation of block delivery before marking content as sent." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and focused: identifies WhatsApp block-streaming was hardcoded, a delivery race that suppressed final replies, and the fix to make streaming configurable with delivery confirmation, without boilerplate or noisy details." + }, + "usage": { + "input_tokens": 1201, + "output_tokens": 532 + } + }, + { + "thread_id": 8098, + "number": 25543, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked a group-level allowlist option, so groups could only be allowed via sender-level groupAllowFrom, preventing explicit group IDs from bypassing sender checks or running control commands.", + "solution_summary": "Adds channels.signal.groups. config with a SignalGroupConfig type and Zod schema, wires resolveChannelGroupPolicy into the Signal event handler to check group-level allowlist before sender-level checks, and incorporates group-level allow into command authorization; documentation updated.", + "maintainer_signal_summary": "Safe, low-risk change following existing patterns used by other channels; TypeScript compilation passes and manual testing was performed. Supersedes PR #4337 and fixes #25540, with group-level allowlist integrated into both message delivery and command gating.", + "dedupe_summary": "Add channels.signal.groups. group-level allowlist support to bypass sender-level groupAllowFrom and allow control commands for explicitly allowed Signal groups." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core change—adding channels.signal.groups. group-level allowlist that bypasses sender-level groupAllowFrom and enables command authorization for allowed Signal groups—without boilerplate or noise." + }, + "usage": { + "input_tokens": 1143, + "output_tokens": 486 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked group-level allowlisting via channels.signal.groups. With groupPolicy set to \"allowlist\" and groupAllowFrom containing group IDs, the access check compared sender IDs to group IDs and always failed, causing group messages to be silently dropped.", + "solution_summary": "Added a groups config to SignalAccountSchemaBase and updated Signal's group access check to call resolveChannelGroupPolicy before falling back to sender-level groupAllowFrom; added four tests to cover group-level policy resolution.", + "maintainer_signal_summary": "Schema change: added groups to SignalAccountSchemaBase (same shape as other channels). Event handler change: Signal now invokes resolveChannelGroupPolicy for group access checks. Tests: 4 new Signal group-policy tests added and local test suites pass. Size: S.", + "dedupe_summary": "Signal missing channels.signal.groups group-level allowlist support causing groupPolicy: \"allowlist\" with group IDs in groupAllowFrom to drop group messages." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it states Signal lacks channels.signal.groups group-level allowlist support, which causes group messages to be dropped when groupPolicy is set to allowlist with group IDs in groupAllowFrom." + }, + "usage": { + "input_tokens": 943, + "output_tokens": 735 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "WhatsApp channel batches all intermediate agent text into a single final message because process-message.ts hardcodes disableBlockStreaming: true, and queuedFinal was used exclusively to mark delivery so intermediate block payloads are ignored.", + "solution_summary": "Replace the hardcoded flag with a resolveWhatsAppBlockStreamingEnabled helper so channels.whatsapp.blockStreaming controls behavior; when enabled intermediate blocks are delivered as separate WhatsApp messages; register the blockStreaming capability and default in the WhatsApp dock and update tests.", + "maintainer_signal_summary": "Backwards compatible by default (omitted/false keeps current behavior); enabling blockStreaming changes message delivery semantics to emit intermediate blocks, so reviewers should check process-message.ts and dock.ts changes and confirm tests and consumer handling.", + "dedupe_summary": "WhatsApp coalesces intermediate message blocks into a single reply due to hardcoded disableBlockStreaming in process-message.ts." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core bug (hardcoded disableBlockStreaming causing WhatsApp to coalesce intermediate message blocks) without boilerplate, so it will cluster with similar reports." + }, + "usage": { + "input_tokens": 693, + "output_tokens": 648 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v8-embedding-aware.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v8-embedding-aware.json new file mode 100644 index 0000000..1a9e956 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v8-embedding-aware.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v8-embedding-aware", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "This text is a GitHub issue or pull request. You must produce a summary optimized for text embedding and cosine-similarity-based duplicate detection.\n\nKey insight: the dedupe_summary will be converted to a 3072-dimensional vector. Similar vectors cluster together. Your goal is to maximize the chance that reports about the SAME underlying issue produce SIMILAR vectors, while reports about DIFFERENT issues produce DISSIMILAR vectors.\n\nWhat helps similarity detection:\n- Specific technical terms (API names, component names, error messages)\n- The core behavior being reported or changed\n- Affected subsystem or module names\n\nWhat hurts similarity detection (creates false matches or misses):\n- Generic words: \"bug\", \"fix\", \"issue\", \"problem\", \"update\", \"change\"\n- Template boilerplate that appears in many unrelated issues\n- Testing details, reproduction steps, environment info\n- Version numbers, dates, PR metadata\n\nStrip all boilerplate. Extract only the technical signal.\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nPlain text, no markdown, 1-3 sentences each.", + "timestamp": "2026-03-30T18:21:09.470Z", + "aggregate": { + "avg_score": 4.58, + "avg_signal_density": 4.55, + "boilerplate_count": 2, + "boilerplate_pct": 5, + "would_cluster_correctly_pct": 100, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 47543, + "total_output_tokens": 34681 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "channels.discord.proxy only proxies the Discord WebSocket Gateway connection; startup REST API calls (deploy commands, fetch bot identity) do not use the proxy and time out (“TypeError: fetch failed”), causing guild resolve to fail and guild channels to be non-functional while DMs work.", + "solution_summary": "Apply the configured proxy to the undici dispatcher used for all Discord REST calls at startup (or set a global ProxyAgent/setGlobalDispatcher early), and ensure guild resolve uses the same proxied HTTP client path; consider adding a global gateway.network.proxy config and retry logic for guild resolve.", + "maintainer_signal_summary": "Patch Discord provider startup to set undici's global dispatcher or pass a ProxyAgent for REST requests when channels.discord.proxy is configured; make guild-resolve use the same proxied HTTP client; add config option (gateway.proxy/network.proxy), tests for proxy environments, and retries for guild resolution.", + "dedupe_summary": "channels.discord.proxy only covers WebSocket Gateway, not REST API; Node.js built-in fetch (undici) ignores HTTP_PROXY/HTTPS_PROXY → TypeError: fetch failed / deploy-rest:put timeout; guild resolve uses separate code path and remains unresolved; workaround: setGlobalDispatcher(new ProxyAgent(...)) fixes REST but not guild resolve; suggested: apply ProxyAgent/undici dispatcher for all Discord REST/guild-resolve, add gateway.proxy or network.proxy config and retry logic." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that pinpoints the core bug (channels.discord.proxy applies only to the Gateway, not REST/guild-resolve), identifies the root cause (undici/Node fetch ignoring HTTP_PROXY), notes a workaround, and proposes fixes — ideal for deduplication." + }, + "usage": { + "input_tokens": 2324, + "output_tokens": 737 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "Outbound Discord REST requests bypass configured channels.discord.proxy: WebSocket/gateway connections via proxy work, but REST sends and attachment uploads fail with TypeError: fetch failed. The Carbon RequestClient fetch implementation appears to lack proxy support, so message sends don’t route through the configured HTTP proxy.", + "solution_summary": "Modify RequestClient (Carbon) to pass a proxy agent to fetch (HTTP/SOCKS) so channels.discord.proxy is honored for all REST endpoints including message sends and attachment uploads; add tests and runtime logs to confirm REST requests use the proxy.", + "maintainer_signal_summary": "Investigate the RequestClient/fetch codepath to inject an HTTP/SOCKS proxy agent or use a proxy-aware fetch implementation, ensure channels.discord.proxy config is applied to REST and media transfers, and add logging/tests to verify proxy usage.", + "dedupe_summary": "channels.discord.proxy not applied to outbound REST sends; Carbon RequestClient fetch lacks proxy agent support; WebSocket/gateway uses proxy but REST message send and attachment upload fail with \"TypeError: fetch failed\"; logs show rest proxy enabled but requests bypass proxy; environment requires HTTP proxy (China)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug—Discord REST requests bypass the configured proxy because the RequestClient/fetch lacks proxy agent support—without checklist or repro noise." + }, + "usage": { + "input_tokens": 1006, + "output_tokens": 881 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "When OpenClaw's Discord provider runs behind an HTTP proxy, the gateway/WebSocket logs show successful login but some startup REST calls consistently fail with 'fetch failed', specifically native command deployment and bot identity fetch.", + "solution_summary": "Ensure Discord REST startup calls use the same proxy-aware HTTP transport as the gateway/login path (unify fetch/dispatcher or attach proxy agent to all Discord REST requests) so PUT /applications/{applicationId}/commands and identity fetch go through the configured proxy.", + "maintainer_signal_summary": "Config key: channels.discord.proxy set (HTTP proxy). Logs show 'rest proxy enabled' and 'gateway proxy enabled', fetch-application-id succeeds, but deploy-rest:put fails for path=/applications/{applicationId}/commands with error=fetch failed and 'failed to fetch bot identity: TypeError: fetch failed'; provider still logs 'logged in to discord' and health-monitor later restarts with reason: stale-socket.", + "dedupe_summary": "OpenClaw Discord provider behind HTTP proxy (channels.discord.proxy) — gateway logs 'logged in to discord' and 'rest proxy enabled' but startup REST calls fail: deploy-rest:put path=/applications/{applicationId}/commands error=fetch failed; fetch-bot-identity fails with 'TypeError: fetch failed'; applicationId fetch succeeds; symptom: native command deployment and bot identity fetch fail while WebSocket gateway works — suggests inconsistent proxy handling/unified fetch/dispatcher between gateway/login transport and REST startup requests; leads to health-monitor stale-socket restarts." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that pinpoints the core issue — inconsistent proxy handling for Discord REST calls vs gateway/WebSocket (native command deploy and bot identity fetch failing while login succeeds) — using specific config/key logs that will match duplicates." + }, + "usage": { + "input_tokens": 1152, + "output_tokens": 831 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "OpenClaw's Discord channel logs show proxy enabled but all Discord REST requests fail with \"fetch failed\" and gatewayConnected=false (WebSocket never establishes) when running under WSL2 with an HTTP proxy configured; curl via the same proxy succeeds. The configured proxy is at ~/.openclaw/openclaw.json → channels.discord.proxy and logs show \"discord: rest proxy enabled\" and \"discord: gateway proxy enabled\" but the underlying REST/fetch client is not using the proxy.", + "solution_summary": "Pass a proper HTTP/HTTPS proxy agent into the Discord REST client and gateway initialization (e.g., create an undici/ProxyAgent or HTTP(s)-proxy-agent from channels.discord.proxy and inject it into discord.js/@discordjs/rest and the gateway/WebSocket), or make the code honor HTTP_PROXY/HTTPS_PROXY environment variables. Add unit/integration tests and a WSL2 proxy scenario to verify REST and gateway connectivity through the proxy.", + "maintainer_signal_summary": "Subsystem: gateway/channels/discord. Config key: channels.discord.proxy (~/.openclaw/openclaw.json). Logs: \"discord: rest proxy enabled\", \"discord: gateway proxy enabled\", errors: \"deploy-rest:put:error ... error=fetch failed\" and \"failed to fetch bot identity: TypeError: fetch failed\"; gatewayConnected=false. Likely cause: discord.js/@discordjs/rest (undici/fetch) not receiving a proxy agent. Fix: construct and inject a ProxyAgent or use global/undici agent; ensure both REST and gateway WebSocket use it and add proxy tests (WSL2).", + "dedupe_summary": "OpenClaw Discord channel: proxy config recognized in logs but discord.js REST calls fail with \"fetch failed\" and gatewayConnected=false under WSL2 using HTTP proxy (127.0.0.1:10808). Config path channels.discord.proxy; probable cause: undici/fetch not using proxy agent; solution: inject ProxyAgent/HTTP(S)_PROXY into REST and gateway clients." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core bug (proxy recognized but undici/fetch-based Discord REST calls fail) with relevant technical terms and probable cause/solution, no template noise." + }, + "usage": { + "input_tokens": 989, + "output_tokens": 1150 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "OpenClaw gateway's Discord REST API requests ignore configured proxy (channels.discord.proxy and HTTP(S)_PROXY env vars) and fail with TypeError: fetch failed; Discord WebSocket and other channels (Telegram) work through the proxy.", + "solution_summary": "Route Discord REST API calls through the same proxy-aware SSRF guard/fetch path used by web_search/web_fetch so REST requests honor channels.discord.proxy and HTTP(S)_PROXY/ALL_PROXY settings.", + "maintainer_signal_summary": "Component: gateway/channels/discord; config key: channels.discord.proxy; env vars: HTTP_PROXY, HTTPS_PROXY, ALL_PROXY; error log: \"discord final reply failed: TypeError: fetch failed\"; curl -x proxy to https://discord.com/api/v10/... returns success; Telegram and Discord WebSocket traffic route through proxy correctly; changelog: web tools routed via proxy-aware SSRF guard in 2026.2.26 but Discord REST not included.", + "dedupe_summary": "Discord REST API ignores proxy; TypeError: fetch failed; gateway/channels/discord; channels.discord.proxy; HTTP_PROXY HTTPS_PROXY ALL_PROXY; curl via proxy to discord.com/api v10 succeeds; Telegram OK; Discord WebSocket OK; missing routing through proxy-aware SSRF guard/fetch path used by web_search/web_fetch; request routing fix: send REST calls through shared proxy-aware fetch." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "The summary clearly and concisely states that Discord REST calls ignore the configured proxy and do not use the shared proxy-aware fetch/SSRF path, which precisely captures the core routing bug for clustering." + }, + "usage": { + "input_tokens": 1012, + "output_tokens": 826 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "openclaw CLI command 'openclaw agent --channel discord --to ...' routes messages to agent:*:main instead of the live Discord channel/thread session because the session resolver ignores delivery context and Discord target normalization.", + "solution_summary": "Patched compiled dist files to resolve sessions by real delivery context (channel + to + accountId), normalize Discord targets (prepend 'channel:'), pass channel and replyAccount/accountId through the command path, and use buildAgentMainSessionKey(...) in the agent resolver.", + "maintainer_signal_summary": "Manual fixes were applied to compiled files in dist (gateway-cli, compact, register.agent) and are high-risk because they'll be overwritten on reinstall; an authoritative source-level fix and post-update revalidation are recommended.", + "dedupe_summary": "CLI session resolver bug: 'openclaw agent --channel discord --to' fails to lookup by delivery context (channel+to+accountId) and fails to normalize Discord IDs, causing routing to agent:*:main; patched compiled files dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js to add delivery-context lookup, Discord 'channel:' normalization, pass channel/replyAccount/accountId, and replace resolveAgentMainSessionKey with buildAgentMainSessionKey; fix should be applied in source to avoid regression on update." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "The summary clearly captures the core bug (CLI resolver not looking up by delivery context and failing to normalize Discord IDs, causing routing to agent:*:main) so it will cluster correctly, but it includes unnecessary implementation/compiled-file details and a deployment note that add minor noise." + }, + "usage": { + "input_tokens": 939, + "output_tokens": 542 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions: Discord threads had volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) re-injected every turn causing progressive context bloat; CLI command openclaw agent --channel discord --to delivered but resolved to agent:*:main because the session resolver didn’t look up by actual delivery context (channel + to + accountId).", + "solution_summary": "Patched runtime bundles in openclaw/dist: dist/pi-embedded-... stopped injecting the listed Discord metadata; dist/gateway-cli-... normalizes Discord targets and resolves sessions by delivery target; dist/compact-... adds delivery-context lookup in the agent session resolver; dist/register.agent-... now passes channel and replyAccount/accountId and replaces resolveAgentMainSessionKey with buildAgentMainSessionKey.", + "maintainer_signal_summary": "These are manual edits to compiled bundles in openclaw/dist that will be overwritten on update — keep a re-application script. Restart openclaw-gateway.service to load changes; verification shows new writer/reasoning sessions with real toolCall output and Discord threads no longer re-loading repeated context. Environment: OpenClaw 2026.3.11, Discord, systemd deployment.", + "dedupe_summary": "Discord threads: volatile metadata re-injected every turn (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) causing context bloat; CLI session routing bug where openclaw agent --channel discord --to resolved to agent:*:main because session resolver didn’t use delivery context (channel+to+accountId). Fixes touch runtime bundles (dist/pi-embedded-*, dist/gateway-cli-*, dist/compact-*, dist/register.agent-*) to stop metadata injection, normalize Discord targets, add delivery-context lookup, pass channel/replyAccount/accountId, and replace resolveAgentMainSessionKey with buildAgentMainSessionKey; manual compiled-bundle patch required on upgrades." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that clearly describes the two technical regressions (repeated volatile metadata in Discord threads and CLI session resolver not using delivery context), their causes, and the specific fixes, with no template boilerplate." + }, + "usage": { + "input_tokens": 858, + "output_tokens": 1202 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "dist/store-*.js hardcodes MEDIA_MAX_BYTES = 5 * 1024 * 1024, so files larger than 5MB cannot be staged into sandbox containers (sandbox.mode: \"non-main\") and are silently rejected or unavailable inside the sandbox.", + "solution_summary": "Expose the sandbox staging cap as a configurable setting (e.g. openclaw.json key tools.media.maxBytes) or apply existing per-type limits earlier so the 5MB constant is not the hard global gatekeeper.", + "maintainer_signal_summary": "Regression present since at least v2026.3.7 and still in v2026.3.8; package is distributed as compiled dist files via npm (npm -g openclaw), currently requiring manual sed patches to dist/store-*.js to increase the cap; affects sandboxed agents and causes silent rejections of large media files.", + "dedupe_summary": "dist/store-*.js defines const MEDIA_MAX_BYTES = 5 * 1024 * 1024 causing sandbox staging to reject files >5MB (sandbox.mode non-main); no override in openclaw.json despite existing tools.media.image.maxBytes and tools.media.audio.maxBytes keys; expected new config key tools.media.maxBytes or earlier per-type limit enforcement; regression since v2026.3.7; npm global distribution packs compiled file." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summarizes the core bug (hardcoded 5 MB MEDIA_MAX_BYTES in compiled dist/store-*.js preventing sandbox staging of larger files and lacking an openclaw.json override) but includes minor noise (version and npm packaging mention) that isn't necessary for deduplication." + }, + "usage": { + "input_tokens": 905, + "output_tokens": 877 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "Discord provider's REST API requests fail with \"fetch failed\" despite proxy configured in openclaw.json and logs showing \"rest proxy enabled\"; Gateway WebSocket works and bot receives messages, but REST operations (deploy native commands, fetch bot identity, send messages) throw TypeError: fetch failed while curl with the same proxy succeeds.", + "solution_summary": "Ensure the Discord REST client actually uses the configured proxy: propagate openclaw.json provider/account proxy into the REST/fetch client (use an HTTP proxy agent for undici/Node fetch or set HTTP_PROXY), add diagnostic logging for chosen HTTP agent and outbound connections, and switch to a proxy-aware fetch agent if necessary.", + "maintainer_signal_summary": "Verify openclaw's discord provider passes proxy to its REST client, reproduce with proxy and log agent selection and connection targets, add tests for provider/account proxy precedence, and consider using/adding support for undici/fetch proxy agent or honoring HTTP_PROXY environment variables.", + "dedupe_summary": "discord REST fetch failed; \"rest proxy enabled\" log present; TypeError: fetch failed; failed to deploy native commands / fetch bot identity; websocket gateway works; curl -x proxy to discord API succeeds; openclaw.json discord.proxy and accounts.default.proxy configured; requests likely not routed through Node/undici fetch proxy agent." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core problem (Discord REST requests failing despite proxy configuration and logs, while gateway and curl with proxy work) and the likely cause (requests not using Node/undici proxy agent), so it will match similar issues accurately." + }, + "usage": { + "input_tokens": 850, + "output_tokens": 995 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "Discord REST requests sent via @buape/carbon's RequestClient (and related paths like memorySearch and fetchRemoteMedia) ignore channels.discord.proxy from openclaw.json: the WebSocket gateway uses the proxy but REST calls call globalThis.fetch without a proxy-aware dispatcher, causing requests to bypass the configured HTTP proxy.", + "solution_summary": "Have @buape/carbon RequestClient accept an optional undici dispatcher/proxy option and have OpenClaw pass channels.discord.proxy into the REST client and other fetch callers (memorySearch, attachment downloads). Short-term workaround: preload a module that calls setGlobalDispatcher(new EnvHttpProxyAgent()), but do not use NODE_OPTIONS=--use-env-proxy.", + "maintainer_signal_summary": "Change RequestClient API to accept a dispatcher/proxy and thread channels.discord.proxy from OpenClaw into all REST/fetch code paths; replace direct globalThis.fetch usage or supply a proxy-aware undici dispatcher; ensure attachment downloads and memorySearch use the same dispatcher. Avoid recommending --use-env-proxy since it breaks WebSocket upgrades.", + "dedupe_summary": "@buape/carbon RequestClient calls globalThis.fetch and ignores openclaw.json channels.discord.proxy; Node.js native fetch (Node 22) doesn't honor HTTP_PROXY/HTTPS_PROXY without --use-env-proxy; WebSocket gateway uses proxy but REST and memorySearch/fetchRemoteMedia bypass it; workaround: undici setGlobalDispatcher(new EnvHttpProxyAgent()) via preload; --use-env-proxy breaks ws WebSocket upgrades (disconnect 1005/1006)." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary clearly identifies that @buape/carbon's RequestClient/globalThis.fetch ignores openclaw.json channels.discord.proxy (while websocket uses it) and notes Node fetch/env-proxy behavior and workaround side-effects, which is useful for clustering though it includes some implementation/workaround detail." + }, + "usage": { + "input_tokens": 859, + "output_tokens": 1131 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "Under sustained load with long-running conversations OpenClaw shows unbounded context token growth (writer sessions ~42k tokens, reasoning sessions ~90k), increasing per-turn latency, Discord lane blocking that serializes work under load, and typing indicators remaining stuck until TTL in some code paths.", + "solution_summary": "Applied configuration limits and timeouts: reduced concurrency (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, agents.defaults.subagents.maxConcurrent 8→4, agents.defaults.subagents.maxChildrenPerAgent 8→4), set agents.defaults.contextTokens=80000, increased channels.discord.eventQueue.listenerTimeout to 300000, improved patching script variants and restarted gateway.", + "maintainer_signal_summary": "Verification: gateway restarted and running, no new 'lane wait exceeded' or 'typing TTL reached' errors observed; patch script updated (openclaw-rootfix-v2026.3.11.sh) to cover reply-*, compact-*, pi-embedded-*, gateway-cli-*, register.agent-* variants. Recommend exposing/adapting context token and concurrency limits as tunable/configurable parameters to prevent recurrence.", + "dedupe_summary": "OpenClaw gateway performance degradation caused by unbounded context token growth (writer ~42k, reasoning ~90k), Discord lane blocking/serialization by agentId, and typing indicators stuck until TTL; mitigations: acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents maxConcurrent 8→4 and maxChildrenPerAgent 8→4, agents.defaults.contextTokens=80000, channels.discord.eventQueue.listenerTimeout=300000; root causes: no context token limit, high concurrency saturating lanes, insufficient event queue timeout." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary clearly captures the core performance issues (unbounded context growth, Discord lane blocking/serialization, typing stuck) and root causes, but includes specific configuration mitigation values that are slightly noisy for deduping." + }, + "usage": { + "input_tokens": 793, + "output_tokens": 835 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every conversation turn, causing context contamination, token bloat, prompt cache invalidation, and session/toolCall regressions.", + "solution_summary": "Manual fix applied to compiled files: dist/pi-embedded-D6PpOsxP.js stops per-turn injection of the listed metadata; dist/compact-1mmJ_KWL.js adds an isNewSession guard to limit Thread starter to the first turn and replaces resolveAgentMainSessionKey with buildAgentMainSessionKey; validation shows writer/reasoning sessions and prompt caching restored.", + "maintainer_signal_summary": "High regression risk — fixes are manual edits to compiled dist/ files and will be lost on update; recommend applying a source-level fix to limit Discord metadata injection to the first turn, add isNewSession guard in the session handler, and add a post-update revalidation script. Affected compiled files: dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js.", + "dedupe_summary": "OpenClaw v2026.3.11 Discord threads per-turn re-injection of volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) in compiled runtime (dist/pi-embedded-D6PpOsxP.js, dist/compact-1mmJ_KWL.js) due to missing isNewSession guard and resolveAgentMainSessionKey misuse, causing context pollution, token inflation and prompt cache bust; manual dist patch prevents injection and uses buildAgentMainSessionKey." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that clearly describes the core bug (per-turn injection of volatile Discord thread metadata), root cause (missing isNewSession/incorrect session key), affected compiled files, and impact (context pollution, token inflation, cache bust), so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 805, + "output_tokens": 1133 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "OpenClaw runtime compiled artifact dist/pi-embedded-D6PpOsxP.js re-injects volatile Discord thread metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, repeated thread-starter/history tokens, prompt-cache busting, token cost increase and degraded performance in long threads.", + "solution_summary": "Manually edited dist/pi-embedded-D6PpOsxP.js to avoid injecting the listed volatile metadata after the first turn (inject only on initial thread message), which restored prompt caching and reduced token usage; recommended to implement the guard in source and upstream the change.", + "maintainer_signal_summary": "High regression risk because the fix modifies a compiled file in dist; maintainers should add the guard in source, add regression tests to prevent per-turn metadata injection for Discord threads, and provide a post-update revalidation script.", + "dedupe_summary": "OpenClaw runtime (dist/pi-embedded-*.js) treats Discord threads as requiring per-turn injection of volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply), causing prompt cache busting, token inflation and degraded performance; temporary manual dist patch prevents re-injection after first turn—needs source-level guard and tests to avoid regressions." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (per-turn injection of volatile Discord thread metadata causing cache busting and token inflation) with minimal noise, though it includes a minor implementation note about a temporary dist patch/tests." + }, + "usage": { + "input_tokens": 813, + "output_tokens": 859 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "Two runtime regressions in Discord threads: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing progressive context growth and latency, and session resolver falling back to agent:*:main so CLI routing resolves the wrong session (openclaw agent --channel discord --to ...).", + "solution_summary": "Applied direct patches to compiled runtime bundles to stop repeated metadata injection and to make resolver lookup use delivery context (channel + to + accountId); deployed idempotent re-application script and backups, restarted gateway and validated thread-bound sessions and logs.", + "maintainer_signal_summary": "Manual edits were applied to compiled dist bundles (reply-*, compact-*, pi-embedded-*, gateway-cli-*, register.agent-*); these patches aren’t preserved on reinstall—maintain the re-application script (/home/clawd/scripts/openclaw-rootfix-v2026.3.11.sh) and backups, and add smoke tests for per-turn context size and latency; gateway runs under systemd and was validated.", + "dedupe_summary": "Discord threads; context growth from per-turn re-injection of message_id, reply_to_id, timestamp, Thread starter, InboundHistory; session resolver fallback to agent:*:main causing CLI routing to wrong session; resolver fix: consistent lookup by delivery context (channel+to+accountId); patched compiled runtime bundles (dist/*: reply, compact, pi-embedded, gateway-cli, register.agent); manual bundle patches lost on update -> idempotent reapply script; systemd openclaw-gateway.service; add smoke tests for latency and context-size." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary that pinpoints two concrete bugs (per-turn metadata re-injection causing context growth and session resolver falling back to global agent), the resolver fix (lookup by delivery context), patched runtime bundles, and operational hardening/tests—no template noise and suitable for deduplication." + }, + "usage": { + "input_tokens": 789, + "output_tokens": 1252 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "OpenClaw 2026.3.2 receives Discord messages and generates responses but fails to send replies with 'TypeError: fetch failed' even though discord proxy is configured and logs show 'rest proxy enabled' and 'gateway proxy enabled'.", + "solution_summary": "Make the Discord REST send path honor the configured HTTP proxy (pass a proxy agent to fetch or respect HTTP_PROXY/HTTPS_PROXY) or update OpenClaw's Discord client to apply the channels.discord.proxy setting for outgoing fetch calls; ensure systemd service exports proxy env. ", + "maintainer_signal_summary": "Regression on Linux systemd (Node.js 22.22.0). openclaw.json has channels.discord.proxy=http://127.0.0.1:7890; logs: 'rest proxy enabled', 'gateway proxy enabled', 'logged in to discord', then 'final reply failed: TypeError: fetch failed'. Incoming messages and model responses are present; curl via same proxy succeeds. ", + "dedupe_summary": "OpenClaw Discord outgoing send failure: 'TypeError: fetch failed' when sending reply despite channels.discord.proxy=http://127.0.0.1:7890 and logs showing rest/gateway proxy enabled; incoming messages and model responses OK; curl via proxy succeeds; Node.js 22.22.0, systemd." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core issue (Discord outgoing sends failing with 'fetch failed' despite proxy settings and logs) and relevant config, but includes minor noisy details (Node.js/systemd version and curl test) that aren't needed for deduping." + }, + "usage": { + "input_tokens": 729, + "output_tokens": 1045 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11, the CLI command 'openclaw agent --channel discord --to ...' resolves to session 'agent:*:main' instead of the live Discord channel/thread session because the compiled session resolver (dist/gateway-cli-BjsM6fWb.js) does not lookup by delivery context (channel+to+accountId) and does not normalize Discord target IDs.", + "solution_summary": "Manually patched dist/gateway-cli-BjsM6fWb.js to normalize Discord targets (e.g. 148... → channel:148...), perform session lookup by delivery context (channel+to+accountId) before falling back to agent:*:main; validated that CLI messages now route to the correct live channel/thread session.", + "maintainer_signal_summary": "Apply the fix in source (not only compiled dist): implement delivery-context-based session lookup and Discord target normalization, add unit/integration tests for '--channel discord --to' routing, and avoid relying on manual edits to dist to prevent regressions.", + "dedupe_summary": "openclaw v2026.3.11 CLI agent --channel discord --to resolves to agent:*:main; root cause: compiled resolver dist/gateway-cli-BjsM6fWb.js lacks delivery-context lookup (channel+to+accountId) and target normalization (148... vs channel:148...), causing fallback to main; manual dist patch adds normalization and lookup; high regression risk because change is in compiled dist." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "The summary concisely states the core bug and technical root cause (missing delivery-context lookup and normalization in the compiled resolver) and would match similar issues, though it includes minor noise like the exact release version and patch/regression notes that aren't necessary for deduplication." + }, + "usage": { + "input_tokens": 794, + "output_tokens": 1090 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "En OpenClaw v2026.3.11 el bundle compilado dist/compact-1mmJ_KWL.js llama a una función inexistente resolveAgentMainSessionKey, provocando ReferenceError en runtime y fallos del comando agent y del session resolver.", + "solution_summary": "Se parcheó dist/compact-1mmJ_KWL.js reemplazando resolveAgentMainSessionKey(...) por buildAgentMainSessionKey(...), lo que elimina el ReferenceError y restaura el comportamiento de agent y las sesiones; la corrección debe aplicarse en el código fuente y reconstruirse para persistir.", + "maintainer_signal_summary": "Corregir la fuente que genera el bundle compact para usar buildAgentMainSessionKey, agregar pruebas que detecten ReferenceError en el session resolver, publicar un build con la corrección y evitar parches directos en dist porque se pierden al reinstalar; esta regresión depende del cambio previo (fix #3).", + "dedupe_summary": "dist/compact-1mmJ_KWL.js; resolveAgentMainSessionKey undefined; buildAgentMainSessionKey; ReferenceError runtime; agent command fails; session resolver regression; compiled dist patch replaced function call; dependent on fix #3; patch lost on reinstall" + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "The summary clearly identifies the core regression (dist/compact-1mmJ_KWL.js calling a non-existent resolveAgentMainSessionKey instead of buildAgentMainSessionKey causing a runtime ReferenceError and agent failures); it’s specific and actionable though it includes a minor extraneous note about dependency on fix #3 and patch persistence on reinstall." + }, + "usage": { + "input_tokens": 765, + "output_tokens": 792 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "Discord channel config channels.discord.proxy is only applied to WebSocket connections, not REST API HTTP calls; as a result openclaw status healthcheck shows 'fetch failed' and openclaw message send times out in proxy-required environments while curl -x through the same proxy succeeds.", + "solution_summary": "Propagate channels.discord.proxy into the Discord REST HTTP client (configure Transport.Proxy or use configured HTTP proxy) and use the same proxy for healthcheck requests; update client initialization and add unit/integration tests to cover proxy scenarios.", + "maintainer_signal_summary": "To reproduce: set channels.discord.proxy, restart gateway, verify curl -x works, then run openclaw message send or openclaw status to observe 'fetch failed'. Inspect the Discord REST client/HTTP transport for missing proxy handling (WebSocket currently uses the proxy) and modify initialization to inject the proxy config.", + "dedupe_summary": "OpenClaw discord plugin: channels.discord.proxy not applied to REST API HTTP client (only applied to WebSocket) causing healthcheck 'fetch failed' and CLI message send failures behind a proxy; curl -x verifies proxy works." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise and specific about the core bug (channels.discord.proxy not applied to REST HTTP client, only WebSocket, causing healthcheck and send failures), but includes a minor testing detail ('curl -x verifies proxy works') that is unnecessary for deduplication." + }, + "usage": { + "input_tokens": 721, + "output_tokens": 889 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "En OpenClaw v2026.3.11 el session resolver dentro de dist/compact-1mmJ_KWL.js del comando `agent` no realiza lookup por delivery context (channel+to+accountId), por lo que resuelve sesiones a `agent:*:main` en lugar de la sesión viva del canal objetivo y las respuestas no se asocian correctamente al hilo/canal.", + "solution_summary": "Parche manual en dist/compact-1mmJ_KWL.js: agregar lookup de sesión por delivery context (channel + to + accountId) y mantener fallback a `agent:*:main` solo si no existe sesión específica.", + "maintainer_signal_summary": "Alta prioridad/regresión: cambio aplicado en archivo compilado en `dist/` (dist/compact-1mmJ_KWL.js) y se perderá en reinstalaciones; requiere correción en código fuente/build para evitar reversiones. Afecta OpenClaw v2026.3.11.", + "dedupe_summary": "session resolver faltando delivery-context lookup (channel+to+accountId) en dist/compact-1mmJ_KWL.js del comando `agent`, causa fallback a agent:*:main, sesiones/respuestas mal asignadas; parche manual en dist compilado; riesgo de pérdida en actualización; versión v2026.3.11." + }, + "judge": { + "score": 4, + "has_boilerplate": true, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary clearly identifies the core bug (missing delivery-context lookup in the agent session resolver in dist/compact-1mmJ_KWL.js causing fallback to agent:*:main and misassigned sessions) but includes minor noisy details (manual patch note and version) that aren't needed for deduplication." + }, + "usage": { + "input_tokens": 735, + "output_tokens": 640 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "OpenClaw Control UI (control-ui) has most interface text hardcoded in English across settings, buttons, agent/skill pages, channel configs, status messages and tooltips, making the UI unusable for Chinese-speaking users.", + "solution_summary": "Extract all hardcoded strings into an i18n layer (JSON locale files), add a zh-CN locale with translated strings, provide a language selector or auto-detect option, and build reusable localization infrastructure for adding more languages.", + "maintainer_signal_summary": "OpenClaw docs already include docs/zh-CN; recommend extracting strings from source (not only dist/control-ui/assets/index-*.js), adopt a standard i18n lib (e.g., i18next or vue-i18n), add zh-CN locale file, and accept contributions to implement the language selector and translations.", + "dedupe_summary": "control-ui hardcoded English strings; extract to i18n JSON locale files; add zh-CN locale; language selector/auto-detect; affected components: Settings, labels, buttons (Add, Remove, Delete, Enable, Disable, Save, Cancel), Agent pages, Skill management, Channel configuration, status messages, tooltips; note dist/control-ui/assets/index-*.js contains hardcoded strings; use i18n/i18next/vue-i18n; goal: Chinese UX accessibility." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core issue (hardcoded English in control-ui), lists affected UI areas and concrete remediation (extract strings, add zh-CN, language selector, use i18n libraries), with no template noise." + }, + "usage": { + "input_tokens": 656, + "output_tokens": 833 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "WhatsApp group chats could not use channel-configured systemPrompt at account or group granularity, preventing per-account and per-group prompt customization and wildcard defaults.", + "solution_summary": "Extended WhatsApp config/schema to support systemPrompt at channels.whatsapp, accounts., and groups. levels; implemented account-level resolution, group prompt composition (account prompt + resolved group prompt joined by blank line), added tests and docs.", + "maintainer_signal_summary": "WhatsApp-only change: adds config keys and resolution logic, tests, and docs; no changes to non-WhatsApp channels, hot-reload, group allowlist/mention-gating, or permissions. Backward compatible; linked to issue #7011.", + "dedupe_summary": "Add support for channels.whatsapp.systemPrompt, channels.whatsapp.accounts..systemPrompt, channels.whatsapp.groups..systemPrompt and channels.whatsapp.accounts..groups..systemPrompt. Resolution: account-level value overrides root; group resolution prefers exact group entry then wildcard \"*\" within the active groups map; account.groups fully replace root.groups (no merge); final injected prompt = account prompt + resolved group prompt (separated by a blank line)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of added WhatsApp systemPrompt config keys and precise resolution/merge rules that capture the core change without boilerplate." + }, + "usage": { + "input_tokens": 2432, + "output_tokens": 674 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were marked read and dropped before entering the inbound/agent path, and WhatsApp group policy resolution ignored the accountId so account-scoped groupAllowFrom/allowFrom were not considered for allowlist fallback.", + "solution_summary": "Append processing is enabled only for WhatsApp group messages and group policy resolution now propagates accountId so account-scoped channels.whatsapp.accounts. config (groupAllowFrom/allowFrom) is used for group gating; changes touch src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts.", + "maintainer_signal_summary": "DM append history remains skipped (no replay of DM reconnection history); targeted tests added for account-scoped allowlist fallback and require-mention behavior; revert by restoring the two touched files if needed. Watch for duplicate processing of historical DMs or divergent group gating between default and account-scoped accounts.", + "dedupe_summary": "messages.upsert(type=\"append\") WhatsApp group messages dropped before enqueue/inbound, group gating returned shouldProcess:false, group route resolved but accountId not propagated causing channels.whatsapp.accounts. groupAllowFrom/allowFrom to be ignored in groupPolicy:\"allowlist\" fallback; files: src/web/inbound/monitor.ts, src/web/auto-reply/monitor/group-activation.ts; preserve DM append skip, concerns about backlog/history processing, signals: web-inbound, web-auto-reply, lane enqueue." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Clearly states the core bug (WhatsApp group append messages dropped before enqueue and accountId not propagated causing account-scoped groupAllowFrom/allowFrom to be ignored) with specific technical terms, though it includes minor noisy details like file paths and signal tags that aren't necessary for deduping." + }, + "usage": { + "input_tokens": 1984, + "output_tokens": 753 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because resolveClientIp() returned undefined for requests from a trusted proxy that omitted X-Forwarded-* headers, and isLocalDirectRequest() did not fall back to the socket remote address when allowRealIpFallback was enabled and XFF resolved to a non-loopback Tailscale IP.", + "solution_summary": "resolveClientIp() now falls back to req.socket.remoteAddress when no forwarding headers exist; isLocalDirectRequest() checks req.socket.remoteAddress when allowRealIpFallback is enabled; isLocalishHost() gates .ts.net host acceptance behind the existing allowTailscale flag; auth.allowTailscale is threaded through callers; docs updated with trustedProxies and allowRealIpFallback guidance.", + "maintainer_signal_summary": "Updated tests in src/gateway/auth.test.ts and src/gateway/net.test.ts; changed files include src/gateway/auth.ts, net.ts, server-http and related auth/ws handlers; gateway.allowTailscale defaults to true when tailscale.mode=\"serve\"; recommend gateway.trustedProxies [\"127.0.0.1\",\"::1\"] and gateway.allowRealIpFallback: true; watch for behavior changes when proxies omit XFF and for .ts.net gate regressions.", + "dedupe_summary": "Canvas/A2UI 401 via Tailscale Serve; resolveClientIp() returned undefined for trusted-proxy direct requests without X-Forwarded headers; isLocalDirectRequest() lacked req.socket.remoteAddress fallback when gateway.allowRealIpFallback=true; isLocalishHost() now requires auth.allowTailscale to accept .ts.net Host headers; relevant configs: gateway.trustedProxies, gateway.allowRealIpFallback, tailscale.mode; key functions: resolveClientIp, isLocalDirectRequest, isLocalishHost." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that identifies the exact failure (401 via Tailscale Serve), root causes, affected functions, and relevant config flags—no template noise and ideal for clustering similar fixes." + }, + "usage": { + "input_tokens": 1823, + "output_tokens": 859 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "In WhatsApp self-chat mode the function checkInboundAccessControl() computes isSelfChat but never enforces it, so non-self messages (groups, DMs from strangers, broadcasts) can pass because resolveWhatsAppRuntimeGroupPolicy() defaults groupPolicy to \"open\" when cfg.channels.whatsapp is defined without explicit groupPolicy.", + "solution_summary": "Added an early-return guard in checkInboundAccessControl() that returns allowed:false for any non-self message when account.selfChatMode === true; the guard runs before group/DM policy evaluation and preserves self-messages via isSamePhone. The change only applies to explicit selfChatMode:true (not auto-detected heuristic) and keeps isSelfChat in the result for logging/UI.", + "maintainer_signal_summary": "Change touches src/web/inbound/access-control.ts; all access-control tests passed. Revert by removing the early-return guard; intentional design: do not use isSelfChat heuristic to avoid blocking teammates when owner appears in allowFrom.", + "dedupe_summary": "WhatsApp self-chat isolation: checkInboundAccessControl() computed isSelfChat but didn't block non-self messages; resolveWhatsAppRuntimeGroupPolicy() defaulted groupPolicy to \"open\", allowing groups/DMs through. Patch adds an early-return guard when account.selfChatMode === true to return allowed:false for non-self (preserving isSamePhone self messages), applied before policy resolution; explicit-config only, heuristic auto-detected self-chat not hard-blocked. File: src/web/inbound/access-control.ts." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary of the bug (isSelfChat computed but not enforced due to default open groupPolicy) and the targeted fix (early-return guard when account.selfChatMode === true applied before policy resolution), without extraneous checklist or repro details." + }, + "usage": { + "input_tokens": 1538, + "output_tokens": 725 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "With groupPolicy: \"allowlist\", messages from senders not in groupAllowFrom are dropped at the access-control layer before mention-gating runs, so non-allowlisted group messages never get stored in the agent's \"Chat messages since your last reply\" context block.", + "solution_summary": "Introduce opt-in groupContextFromAll (top-level and per-account) so access-control returns storeForContext for non-allowlisted senders; inbound monitor marks those messages contextOnly, skips read receipts, records them via recordPendingHistoryEntryIfEnabled, and stores them as pending context without triggering replies.", + "maintainer_signal_summary": "Changes touch access-control.ts, monitor.ts, types.ts, on-message.ts, config types and zod schema, and accounts; default false preserves backward compatibility; tested locally; risk: allowlist matching/normalization may misclassify senderE164 and the context-only early path may bypass existing group gating/state updates (confidence ~3/5).", + "dedupe_summary": "WhatsApp change: add groupContextFromAll to preserve non-allowlisted group messages as pending context instead of drop. Key tokens: groupPolicy \"allowlist\", groupAllowFrom, groupContextFromAll, access-control -> storeForContext outcome, contextOnly WebInboundMessage, recordPendingHistoryEntryIfEnabled, skip read receipts, no auto-reply trigger; modified files include src/web/inbound/access-control.ts, monitor.ts, types.ts, src/web/auto-reply/monitor/on-message.ts, src/config/types.whatsapp.ts, zod schema, src/web/accounts.ts; risk: allowlist normalization/matching on senderE164." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Clearly describes the core change (preserve non-allowlisted group messages as pending context via new groupContextFromAll and related access-control changes) with specific technical tokens, so it will cluster well despite minor extra file-level detail." + }, + "usage": { + "input_tokens": 1555, + "output_tokens": 892 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "Adds optional sessionKey parameter to sessions_spawn to let sub-agents reuse deterministic sessions instead of generating a new crypto.randomUUID() each spawn; short keys are namespaced to agent:{agentId}:subagent:{key} and fully-qualified keys are validated to match the target agentId. Tests were added but one test imports a non-existent module (openclaw-tools.js), causing compilation failure.", + "solution_summary": "Behavior: if sessionKey omitted fallback to crypto.randomUUID(); short keys map to agent:{agentId}:subagent:{key}; fully-qualified keys (contain ':subagent:') are permitted only if embedded agentId matches normalizeAgentId(targetAgentId) to prevent cross-agent session injection. Fixes: correct the broken test import, normalize embedded agentId case before comparison, and use consistent jsonResult({status:'forbidden',...}) error handling instead of throwing to match existing checks.", + "maintainer_signal_summary": "Blocking test compile: src/agents/openclaw-tools.subagents.sessions-spawn-reuses-session-key.test.ts imports createOpenClawTools from ./openclaw-tools.js (should match other tests' createClawdbotTools ./clawdbot-tools.js). Code file: src/agents/tools/sessions-spawn-tool.ts needs lowercase/normalize embedded agentId for comparison and replace throw new Error(...) with jsonResult({status:'forbidden',...}) for consistent error surface.", + "dedupe_summary": "sessions_spawn: add sessionKey to reuse sub-agent sessions; short keys -> agent:{agentId}:subagent:{key}; fully-qualified keys must contain ':subagent:' and embedded agentId must equal normalizeAgentId(targetAgentId) to prevent cross-agent session injection; current bugs: case-sensitivity mismatch (embedded id not lowercased), inconsistent error handling (throw vs jsonResult), and failing test import openclaw-tools.js vs clawdbot-tools.js." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summarizes the new sessionKey behavior and validation precisely (good for dedupe), but includes implementation bug/test-import details that are useful for PR context yet are minor noise for clustering." + }, + "usage": { + "input_tokens": 1608, + "output_tokens": 1142 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) failed: code called editMessageText and Telegram returned 400 \"there is no text in the message to edit\", causing message(action=edit) to throw and UI inconsistency.", + "solution_summary": "send.ts::editMessageTelegram now catches the specific 400 (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption for captionable media; buttons-only updates use editMessageReplyMarkup directly; HTML-parse fallback preserved and MESSAGE_NOT_MODIFIED is treated as success.", + "maintainer_signal_summary": "Six new unit tests added (auto-detect retry, buttons-only, error propagation, MESSAGE_NOT_MODIFIED); only src/telegram/send.ts changed; backward compatible with no interface changes; risk: regex may break if Telegram error wording changes; revert by restoring previous send.ts implementation.", + "dedupe_summary": "Telegram edit failure: editMessageText -> 400 \"there is no text in the message to edit\"; fix retries with editMessageCaption; buttons-only -> editMessageReplyMarkup; NO_TEXT_IN_MESSAGE_RE regex; MESSAGE_NOT_MODIFIED swallowed; file: src/telegram/send.ts editMessageTelegram; media: photo, video, audio, document, animation; unit tests added." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the bug and fix—detecting captionable Telegram media edits by retrying with editMessageCaption (and handling buttons-only via editMessageReplyMarkup), with relevant symbols/files noted—no boilerplate or noisy details." + }, + "usage": { + "input_tokens": 1489, + "output_tokens": 720 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "PR #24962 hardcoded disableBlockStreaming: true for WhatsApp, which disabled block streaming entirely so messages were batched and sent only when the agent finished even if blockStreamingDefault: \"on\" and blockStreamingBreak: \"text_end\" were configured.", + "solution_summary": "Changed deliver callback to suppress only payloads with kind === \"tool\"; make disableBlockStreaming driven by per-account channels.whatsapp.blockStreaming (falling back to agents.defaults.blockStreamingDefault); reasoning payloads continue to be filtered upstream by shouldSuppressReasoningPayload.", + "maintainer_signal_summary": "Touched files: extensions/whatsapp/src/auto-reply/monitor/process-message.ts and dispatch-from-config.ts (shouldSuppressReasoningPayload at dispatch-from-config.ts:446). Inbound contract tests pass; to revert or disable, set channels.whatsapp.blockStreaming:false or restore process-message.ts. Verify no reasoning/tool payloads leak to WhatsApp.", + "dedupe_summary": "WhatsApp blockStreaming was hardcoded off (disableBlockStreaming:true) causing batched delivery; fix makes disableBlockStreaming config-driven (channels.whatsapp.blockStreaming -> agents.defaults.blockStreamingDefault) and narrows deliver suppression to kind==='tool'; upstream shouldSuppressReasoningPayload in dispatch-from-config.ts still blocks reasoning; files: process-message.ts, dispatch-from-config.ts; behavior: progressive delivery at blockStreamingBreak:text_end vs previous batching." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core bug (hardcoded disableBlockStreaming causing batching), the fix (make it config-driven and limit deliver suppression to kind==='tool'), and relevant files/behavior so it will cluster with similar WhatsApp streaming issues." + }, + "usage": { + "input_tokens": 1481, + "output_tokens": 849 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Telegram gateway hangs when receiving video messages larger than the old 5MB default: fetchRemoteMedia triggers a content-length check ('exceeds maxBytes 5242880') that leaves the response body unconsumed, causing closeDispatcher()/undici Agent.close() to deadlock and the gateway to become unresponsive.", + "solution_summary": "Increase default channels.telegram.mediaMaxMb from 5 to 20; make media download errors non-fatal so text/captions still forward; add timeouts and retry logic to getFile and download flows; change closeDispatcher to use a 3s timeout then destroy the Agent to avoid undici deadlock; update stripInboundMetadata to strip inbound media paths.", + "maintainer_signal_summary": "Touched files: src/infra/net/ssrf.ts, src/telegram/bot.ts; config key: channels.telegram.mediaMaxMb (default now 20); tests updated (strip-inbound-meta.test.ts passes); behavior: inbound media saved to ~/.openclaw/media/inbound/, >20MB yields graceful warning; no SSRF policy or storage changes.", + "dedupe_summary": "Telegram gateway hang on large media: fetchRemoteMedia content-length > maxBytes (5242880) left response body unconsumed -> closeDispatcher()/undici Agent.close() deadlock; fixes: raise channels.telegram.mediaMaxMb 5->20, make media errors non-fatal, add getFile/download timeouts and retries, closeDispatcher 3s timeout + destroy fallback, stripInboundMetadata updated." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Clearly states the root cause (unconsumed response body causing undici Agent.close deadlock) and the specific fixes (raise media limit, non-fatal media errors, timeouts/retries, closeDispatcher fallback, metadata handling), so it will cluster correctly with same issues." + }, + "usage": { + "input_tokens": 1577, + "output_tokens": 851 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "In the Telegram integration, resolveTelegramInboundBody’s fallback branch used a hardcoded '' when no caption/text was present, causing document attachments (PDF/DOCX) to be misidentified as images so the agent failed to recognize documents.", + "solution_summary": "Changed resolveTelegramInboundBody to use the existing type-aware placeholder variable (which yields '', '', etc.) instead of ''; added unit tests to lock in correct placeholders; media download/extraction and understanding pipeline unchanged.", + "maintainer_signal_summary": "Unit tests added in extensions/telegram/src/bot/helpers.test.ts and extensions/telegram/src/bot.create-telegram-bot.test.ts to cover resolveTelegramMediaPlaceholder for all media types and to verify document messages trigger getFile; change is limited to agent-facing body placeholder and is backward compatible.", + "dedupe_summary": "resolveTelegramInboundBody previously hardcoded '' instead of using the computed placeholder variable, mislabeling documents; affected code paths: hasInboundMedia, resolveInboundMediaFileId, getFile invocation. Tests added: extensions/telegram/src/bot/helpers.test.ts and bot.create-telegram-bot.test.ts; media types: document (PDF/DOCX), photo, video, audio, voice, sticker; placeholder strings: '' -> ''." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary clearly identifies the core bug (hardcoded '' vs. computed placeholder causing mislabeling of document/media types) and affected code paths, with minor noise from test filenames and media type lists that don't hurt clustering." + }, + "usage": { + "input_tokens": 1404, + "output_tokens": 860 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "src/media/store.ts contains a hardcoded MEDIA_MAX_BYTES = 5 MB constant so files larger than 5 MB silently fail to stage into sandbox containers, preventing common file types (PDFs, presentations, high-res images) from being available to sandboxed agents. Per-channel download limits and tools.media.* limits are unchanged.", + "solution_summary": "Introduced a new optional config field media.maxBytes (integer bytes, default 5 MB, max 500 MB); replaced hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes() and gateway boot reads the config and sets the store limit; error messages now display the actual configured limit and MEDIA_MAX_BYTES export kept (deprecated) for compatibility.", + "maintainer_signal_summary": "Unit tests updated/passing (store.test.ts, server.test.ts, stage-sandbox-media.test.ts, schema.help.quality.test.ts); addressed review threads by always calling setMediaMaxBytes with a fallback default and adding formatBytesAsMb() for precise error text; schema enforces <=500 MB and default behavior preserved when config absent.", + "dedupe_summary": "Hardcoded MEDIA_MAX_BYTES (5 MB) in src/media/store.ts blocks staging >5MB files into sandbox containers; add configurable media.maxBytes (bytes) read at gateway boot via getMediaMaxBytes()/setMediaMaxBytes(), enforce schema max 500MB, update error text to show configured limit; per-channel download limits unchanged." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that identifies the hardcoded 5MB limit, the exact file and functions changed, the new configurable media.maxBytes behavior, and notes unchanged per-channel limits—no boilerplate or unrelated details." + }, + "usage": { + "input_tokens": 1525, + "output_tokens": 785 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "Telegram media download failures (exceeding mediaMaxMb or Telegram getFile API errors) caused the entire incoming message to be silently dropped, leaving the agent unaware, no ack reaction, and losing any caption/text.", + "solution_summary": "On download failure, extract file metadata (name, size, type) from the Telegram message object, inject a metadata stub into msg.text (e.g., \"⚠️ [File: name, 40.0MB] (exceeds 5MB download limit)\"), and let the message continue to processMessage instead of returning early.", + "maintainer_signal_summary": "Change implemented in src/telegram/bot-handlers.ts; removed early returns on download error paths and added buildFileMetadataStub to mutate msg.text for error-path messages; normal media download path, media group handling, sticker processing, and outbound file sending unchanged; revert by reverting the single commit on bot-handlers.ts.", + "dedupe_summary": "Telegram media download failure -> silent message drop; mediaMaxMb (default 5MB) and Telegram Bot API getFile 20MB limit; inject file metadata stub into msg.text on error (buildFileMetadataStub) so processMessage receives messages including documents/photos/videos/audio/voice/photo_note/sticker with preserved captions; fixes missing ack reaction and lost messages caused by early return in src/telegram/bot-handlers.ts." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the bug and fix — preserves file metadata on Telegram download failure by injecting a stub so messages (documents/photos/voice/stickers, with captions) are not dropped and ack reactions aren't missed; no template noise." + }, + "usage": { + "input_tokens": 1367, + "output_tokens": 693 + } + }, + { + "thread_id": 5796, + "number": 24117, + "kind": "pull_request", + "title": "feat(cli): add --session-key targeting for openclaw agent", + "summary": { + "problem_summary": "openclaw agent CLI has no --session-key option, so CLI-triggered deliveries in callback/automation flows often resolve to the agent main session because sessionId is UUID-based and --to is sender-derived and not always available.", + "solution_summary": "Added --session-key flag in src/cli/program/register.agent.ts, added sessionKey?: string to AgentCliOpts and forwarded it into resolveSessionKeyForRequest via src/commands/agent-via-gateway.ts, preserving resolution precedence (--session-key, --session-id, --to, --agent) and updating help/validation; added tests for parsing and forwarding.", + "maintainer_signal_summary": "Key files: src/cli/program/register.agent.ts and src/commands/agent-via-gateway.ts; tests: src/cli/program/register.agent.test.ts and src/commands/agent-via-gateway.test.ts; change is additive/opt-in, backward-compatible, local tests passing.", + "dedupe_summary": "openclaw agent --session-key sessionKey AgentCliOpts resolveSessionKeyForRequest agent-via-gateway register.agent.ts register.agent.test.ts agent-via-gateway.test.ts precedence --session-key > --session-id > --to > --agent stable channel session targeting callback routing UUID sessionId" + }, + "judge": { + "score": 3, + "has_boilerplate": true, + "signal_density": 3, + "would_cluster_correctly": true, + "reasoning": "Summary identifies the core change (adding --session-key and its precedence to target stable channel sessions vs UUID sessionId) but is cluttered with filenames, test names, and token noise that should be removed for cleaner deduplication." + }, + "usage": { + "input_tokens": 1339, + "output_tokens": 785 + } + }, + { + "thread_id": 766, + "number": 11611, + "kind": "pull_request", + "title": "feat: separate group-level allowlist from sender-level command authorization", + "summary": { + "problem_summary": "With groupPolicy:\"allowlist\" the code currently uses groupAllowFrom (sender phone numbers) to gate all group interactions, preventing per-group JID allowlisting, preventing open chat for everyone in an approved group, and conflating command authorization with group admission (bot can be pulled into spam groups).", + "solution_summary": "If channels.whatsapp.groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access control now checks the group JID allowlist (via resolveChannelGroupPolicy) to admit groups, allows any participant in an approved group to chat, and keeps groupAllowFrom as the sender-level gate solely for slash-command authorization; when no groups mapping exists, behavior falls back to legacy sender-based filtering.", + "maintainer_signal_summary": "Code changes localized to src/web/inbound/access-control.ts (integrates resolveChannelGroupPolicy/checkInboundAccessControl) and JSDoc in src/config/types.whatsapp.ts; adds vitest file src/web/inbound/access-control.group-allowlist.test.ts with four tests covering approved-group admission, blocked groups, legacy sender fallback, and command-authority separation; depends on #3326; one stale inline comment in access-control.ts noted for update.", + "dedupe_summary": "WhatsApp groupPolicy allowlist: when channels.whatsapp.groups mapping is present, gate inbound group messages by group JID allowlist (resolveChannelGroupPolicy) instead of sender-based groupAllowFrom; groupAllowFrom still authorizes slash commands (/new, /reset); fallback to sender-based filtering if no groups mapping; files: src/web/inbound/access-control.ts, src/config/types.whatsapp.ts; tests: access-control.group-allowlist.test.ts." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that clearly describes the behavioral change (use group JID allowlist for group messages, keep sender-based auth for slash commands, fallback behavior) but includes minor noise like filenames/tests that aren't needed for deduping." + }, + "usage": { + "input_tokens": 1389, + "output_tokens": 855 + } + }, + { + "thread_id": 17924, + "number": 46425, + "kind": "pull_request", + "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", + "summary": { + "problem_summary": "Configured telegramAccounts[].mediaMaxMb (mediaMaxBytes) was ignored for outbound reply messages: loadWebMedia() on the reply delivery path received no maxBytes and fell back to the 100 MB default, while the direct-send path already honored the limit.", + "solution_summary": "Propagated mediaMaxBytes through the Telegram outbound reply chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia receives maxBytes; added unit tests verifying propagation and undefined behavior when unset.", + "maintainer_signal_summary": "108 unit tests across delivery, dispatch, native-commands, and bot-message pass; change is backward-compatible, limited to threading a parameter, send.ts and media loading internals unchanged; revertable by a single commit.", + "dedupe_summary": "telegram mediaMaxMb mediaMaxBytes loadWebMedia outbound reply delivery missing maxBytes fallback 100MB threaded through bot.ts bot-message.ts bot-message-dispatch.ts delivery.replies.ts bot-native-commands.ts direct-send send.ts already correct unit tests delivery.test.ts 'passes mediaMaxBytes to loadWebMedia' 'omits maxBytes from loadWebMedia'" + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that states the core bug (mediaMaxMb/mediaMaxBytes not propagated so loadWebMedia falls back to 100MB for outbound Telegram replies) and lists the specific files changed, with no template boilerplate." + }, + "usage": { + "input_tokens": 1390, + "output_tokens": 756 + } + }, + { + "thread_id": 5712, + "number": 37050, + "kind": "pull_request", + "title": "fix(telegram): add HTML parse fallback for media captions", + "summary": { + "problem_summary": "Media send functions (sendPhoto, sendDocument, sendVideo, sendAnimation, sendAudio, sendVoice, video_note) lacked the HTML-parse fallback used for text messages; captions with invalid HTML entities caused Telegram 400 \"can't parse entities\" errors and the entire media send to fail.", + "solution_summary": "Wrapped the media send callback in withTelegramHtmlParseFallback so on HTML parse errors the request is retried without parse_mode (HTML) using the raw caption; change applied in src/telegram/send.ts to cover all media types.", + "maintainer_signal_summary": "Added unit test that mocks sendPhoto to first reject with parse error then succeed, asserting initial call had parse_mode: \"HTML\" and retry had no parse_mode; edit message, text path, thread fallback, and caption-splitting logic unchanged; revert by restoring send.ts sendMedia wrapper.", + "dedupe_summary": "Telegram media captions HTML parse fallback: withTelegramHtmlParseFallback; affected APIs sendPhoto, sendDocument, sendVideo, sendAnimation, sendAudio, sendVoice, video_note; failure: 400 \"can't parse entities\" when parse_mode: HTML; behavior: retry without parse_mode using raw caption; file: src/telegram/send.ts; test: mock sendPhoto reject then succeed asserting parse_mode removed on retry." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Clear, specific summary of adding an HTML-parse fallback for Telegram media captions and the retry behavior on 400 parse errors, but it includes minor noise (file path and test detail) that isn't necessary for deduping." + }, + "usage": { + "input_tokens": 1264, + "output_tokens": 783 + } + }, + { + "thread_id": 21769, + "number": 55280, + "kind": "pull_request", + "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", + "summary": { + "problem_summary": "WhatsApp block streaming was effectively disabled (disableBlockStreaming hardcoded true) so intermediate block replies were batched and a delivery race caused final replies to be suppressed: the pipeline marked sentContentKeys on optimistic enqueue before actual socket delivery, so WhatsApp could receive neither blocks nor final reply.", + "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming and wire disableBlockStreaming from account config; add sendBlockReplyAsync (resolves/rejects on confirmed delivery) and await it in onBlockReply; add blockStreamingCoalesceDefaults and update tests and mocks to ensure sentContentKeys is set only after confirmed delivery.", + "maintainer_signal_summary": "Files changed: process-message.ts (config wiring, disableBlockStreaming fix), shared.ts (blockStreamingCoalesceDefaults), reply-dispatcher.ts (sendBlockReplyAsync, onSettled), dispatch-from-config.ts (await dispatcher.sendBlockReplyAsync), tests updated including dispatch-from-config.test.ts and mock dispatchers; manual verification and config CLI command included.", + "dedupe_summary": "WhatsApp delivery race: optimistic dispatcher.sendBlockReply() enqueued item and pipeline set sentContentKeys before socket confirmation causing suppressed final reply; fix: await dispatcher.sendBlockReplyAsync() that resolves/rejects on confirmed delivery, wire disableBlockStreaming from channels.whatsapp.blockStreaming, add blockStreamingCoalesceDefaults, update process-message.ts, reply-dispatcher.ts, dispatch-from-config.ts and tests." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the WhatsApp delivery race root cause and the concrete fixes (awaiting confirmed block delivery and wiring blockStreaming config), with only relevant file/setting details for clustering." + }, + "usage": { + "input_tokens": 1177, + "output_tokens": 812 + } + }, + { + "thread_id": 8098, + "number": 25543, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked group-level allowlist support: only sender-level groupAllowFrom existed, so specific Signal group IDs could not be explicitly allowed to bypass sender checks or run control commands.", + "solution_summary": "Add channels.signal.groups. config and SignalGroupConfig type; add SignalGroupSchema and groups field to Zod schema; wire resolveChannelGroupPolicy into signal event handler and commandGate authorizers so group-level allowlist bypasses sender-level groupAllowFrom for message delivery and command authorization; update docs.", + "maintainer_signal_summary": "TypeScript compile (tsc --noEmit) passes and manual Signal testing performed. Addresses Greptile review by incorporating explicit group allow into command authorization (commandGate). Supersedes PR #4337 and fixes issue #25540.", + "dedupe_summary": "Add channels.signal.groups. allowlist; SignalGroupConfig (requireMention, tools, toolsBySender); SignalAccountConfig.groups and SignalGroupSchema in Zod schema; resolveChannelGroupPolicy wired into src/signal/monitor/event-handler and used by commandGate authorizers; uses hasGroupAllowFrom param to preserve backward behavior; groups listed bypass sender-level groupAllowFrom checks and are authorized to run control commands." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that clearly states the core change—adding a Signal group-level allowlist/config and wiring it into authorization—without template noise, so it will cluster with similar changes." + }, + "usage": { + "input_tokens": 1119, + "output_tokens": 880 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal lacked group-level allowlisting via channels.signal.groups., so with groupPolicy: \"allowlist\" and groupAllowFrom containing group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter and sender phone/UUID never matches a group ID.", + "solution_summary": "Added groups config to SignalAccountSchemaBase (same shape as other channels) and inserted a resolveChannelGroupPolicy call into Signal's group access check (before the sender-level isSenderAllowed fallback). Added four tests to verify Signal group-policy resolution.", + "maintainer_signal_summary": "Changed SignalAccountSchemaBase to include groups (requireMention, tools, toolsBySender); updated Signal event handler/resolveDmGroupAccessDecision to call resolveChannelGroupPolicy before isSenderAllowed(effectiveGroupAllowFrom); added 4 tests; all related tests pass; fixes #25540.", + "dedupe_summary": "Signal missing resolveChannelGroupPolicy caused groupPolicy:allowlist to use groupAllowFrom (a sender-level filter) against sender phone/UUID, so group IDs in channels.signal.groups. were ignored and group messages dropped; fix adds SignalAccountSchemaBase.groups and calls resolveChannelGroupPolicy in Signal access check." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it identifies the core bug (group allowlist misuse of sender-level filter), the symptom (group messages dropped), and the fix (add groups schema and call resolveChannelGroupPolicy), without boilerplate." + }, + "usage": { + "input_tokens": 919, + "output_tokens": 629 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "process-message.ts hardcodes disableBlockStreaming: true for WhatsApp, causing intermediate agent text/tool blocks to be coalesced into a single final WhatsApp message; intermediate payloads are not counted because queuedFinal was used exclusively to mark delivery.", + "solution_summary": "Add resolveWhatsAppBlockStreamingEnabled() and drive behavior from channels.whatsapp.blockStreaming; replace hardcoded disableBlockStreaming, register blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in dock.ts, and treat intermediate block payloads as valid replies when enabled; tests updated accordingly.", + "maintainer_signal_summary": "process-message.inbound-contract.test.ts updated to mock counts like other channels; all 10 tests pass; change is backward-compatible by default (blockStreaming false); labels: channel: whatsapp-web, app: web-ui, agents, size: S.", + "dedupe_summary": "WhatsApp currently forces disableBlockStreaming:true in process-message.ts so intermediate blocks are coalesced and queuedFinal alone marks delivery; PR adds resolveWhatsAppBlockStreamingEnabled() to use channels.whatsapp.blockStreaming, registers blockStreaming and DEFAULT_BLOCK_STREAMING_COALESCE in dock.ts, and makes intermediate blocks count as delivered when blockStreaming:true (false/omitted preserves legacy coalescing)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change (config-driven WhatsApp block streaming and treating intermediate blocks as delivered) with relevant filenames/functions and no boilerplate." + }, + "usage": { + "input_tokens": 669, + "output_tokens": 838 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v9-issue-vs-pr.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v9-issue-vs-pr.json new file mode 100644 index 0000000..640abe2 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/results/v9-issue-vs-pr.json @@ -0,0 +1,940 @@ +{ + "experiment_id": "v9-issue-vs-pr", + "model": "gpt-5-mini", + "judge_model": "gpt-5-mini", + "system_prompt": "Summarize this GitHub issue or pull request for duplicate detection clustering.\n\nIf this is an ISSUE:\n- Extract: What specific problem is reported? Which component/API/feature is broken?\n- Ignore: reproduction steps, expected vs actual behavior headings, environment details, template fields\n- dedupe_summary should describe THE PROBLEM in specific technical terms\n\nIf this is a PULL REQUEST:\n- Extract: What specific problem does this fix? What change is being made and to what component?\n- Ignore: testing instructions, review checklists, deployment notes, CI status, diff statistics\n- dedupe_summary should describe THE FIX/CHANGE and what problem it addresses\n\nIn both cases, strip template boilerplate, checklists, and any content that would appear identically in unrelated issues/PRs.\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nPlain text, no markdown, 1-3 sentences each. dedupe_summary is most important — it must uniquely identify this topic.", + "timestamp": "2026-03-30T18:38:58.926Z", + "aggregate": { + "avg_score": 4.75, + "avg_signal_density": 4.75, + "boilerplate_count": 1, + "boilerplate_pct": 3, + "would_cluster_correctly_pct": 100, + "total_scored": 40, + "total_errors": 0, + "total_input_tokens": 46676, + "total_output_tokens": 26968 + }, + "results": [ + { + "thread_id": 15126, + "number": 49990, + "kind": "issue", + "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", + "summary": { + "problem_summary": "channels.discord.proxy only applies to the WebSocket gateway; startup REST API calls (deploy-rest, fetch-bot-identity) and guild resolution do not use the configured HTTP proxy, causing REST requests to time out (TypeError: fetch failed) and guild channels to remain unresolved in proxy-required environments.", + "solution_summary": "Ensure the configured proxy is applied to the undici/fetch dispatcher used for all Discord REST API calls and the guild-resolve code path during startup (or add a global proxy config), and add retry logic for guild resolution so REST and guild channels work behind an HTTP proxy.", + "maintainer_signal_summary": "Logs show WebSocket connects via proxy but REST calls fail with 'fetch failed' after ~10s and 'channels unresolved: guild:'; Node.js 25.8.0 (undici) fetch ignores HTTP_PROXY; setGlobalDispatcher(new ProxyAgent(...)) makes REST succeed but guild resolve still fails; OpenClaw 2026.3.13.", + "dedupe_summary": "Bug: channels.discord.proxy only proxies Discord Gateway websockets while REST API calls and the guild-resolve path bypass the proxy, causing startup REST timeouts ('fetch failed') and unresolved guild:; workaround using undici ProxyAgent fixes REST but not guild resolve — fix by applying ProxyAgent/global undici dispatcher to all Discord REST and guild-resolve code paths." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that identifies the core bug (proxy only applied to Gateway, REST and guild-resolve bypass it causing timeouts) and the required fix (use undici ProxyAgent/dispatcher for all Discord REST and guild-resolve paths), so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 2302, + "output_tokens": 728 + } + }, + { + "thread_id": 8920, + "number": 30244, + "kind": "issue", + "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", + "summary": { + "problem_summary": "Outbound Discord REST requests (sending messages/attachments) fail when running behind a proxy: channels.discord.proxy is configured but REST sends error with TypeError: fetch failed while the gateway (WebSocket) receives messages successfully.", + "solution_summary": "Add proxy support to the Carbon RequestClient.fetch or otherwise ensure channels.discord.proxy is applied to all outbound Discord REST/HTTP requests (including message sends and attachment uploads) so REST traffic is routed through the configured proxy.", + "maintainer_signal_summary": "Reproducible on OpenClaw 2026.2.25/2026.2.26 in a China/restricted network using http://127.0.0.1:7890/; logs claim 'rest proxy enabled' but REST calls bypass the proxy and fail, causing high-impact failures for users behind proxies/VPNs.", + "dedupe_summary": "channels.discord.proxy is ignored for outbound Discord REST API calls because Carbon's RequestClient.fetch has no proxy support, causing fetch failed errors on message send/attachment uploads in proxy-required environments (OpenClaw 2026.2.25/2026.2.26)." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that identifies the core bug (channels.discord.proxy ignored for outbound REST because Carbon's RequestClient.fetch lacks proxy support) but includes unnecessary version numbers that are irrelevant for deduplication." + }, + "usage": { + "input_tokens": 984, + "output_tokens": 761 + } + }, + { + "thread_id": 19616, + "number": 51018, + "kind": "issue", + "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", + "summary": { + "problem_summary": "When OpenClaw's Discord provider is configured behind an HTTP proxy, the provider can log in and connect the gateway/WebSocket, but some startup REST calls fail with “fetch failed”. Specifically, native command deployment (PUT /applications/.../commands) and bot identity fetch return errors despite application ID fetch and gateway connectivity succeeding.", + "solution_summary": "Ensure all Discord REST startup calls use the same proxy-aware fetch/dispatcher as the gateway/login path; unify or fix transport/proxy handling so native command deployment and bot identity fetch go through the configured HTTP proxy.", + "maintainer_signal_summary": "User provided reproducible logs and environment: OpenClaw 2026.3.12 (touched by 2026.3.13), Ubuntu 24.04, channels.discord.proxy set to http://127.0.0.1:8119; logs show rest proxy enabled, applicationId fetch succeeds, gateway connects, but deploy-rest and fetch-bot-identity fail repeatedly with \"fetch failed\" and later stale-socket restarts; user can help test patches.", + "dedupe_summary": "OpenClaw Discord provider behind HTTP proxy: native command deployment (PUT /applications/:id/commands) and bot identity REST calls fail with \"fetch failed\" while gateway login/WebSocket succeeds, indicating inconsistent or missing proxy handling for some REST request paths (OpenClaw 2026.3.12/3.13, channels.discord.proxy configured)." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary clearly identifies the core problem—Discord REST endpoints (command deployment and identity fetch) failing behind an HTTP proxy while gateway login succeeds—though it includes minor noise like version/config details that aren't needed for deduping." + }, + "usage": { + "input_tokens": 1130, + "output_tokens": 632 + } + }, + { + "thread_id": 16324, + "number": 47670, + "kind": "issue", + "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", + "summary": { + "problem_summary": "Discord channel's configured HTTP proxy (channels.discord.proxy) appears in logs as enabled but the Discord REST client/fetch still fails with 'fetch failed' and the gateway never connects under WSL2. The broken component is the OpenClaw Discord integration (discord.js REST/fetch and gateway connection) on OpenClaw 2026.3.13.", + "solution_summary": "The Discord integration should actually pass and use the configured HTTP proxy (e.g., attach an HTTP proxy agent or configure fetch) for discord.js REST and gateway connections so REST calls succeed and the WebSocket gateway can establish. Currently the proxy is logged as enabled but not applied to the underlying HTTP client.", + "maintainer_signal_summary": "Logs show 'discord: rest proxy enabled' and 'discord: gateway proxy enabled' while REST calls error with 'deploy-rest:put:error ... error=fetch failed' and 'failed to fetch bot identity: TypeError: fetch failed'; gatewayConnected=false and WebSocket never establishes. Curl via the proxy to Discord returns 200, OS is WSL2, OpenClaw 2026.3.13.", + "dedupe_summary": "OpenClaw 2026.3.13 Discord channel proxy not applied: channels.discord.proxy is logged as enabled but discord.js REST/fetch and gateway fail with 'fetch failed' under WSL2 (proxy http://127.0.0.1:10808), preventing REST API calls and WebSocket connection." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary concisely captures the core bug (Discord channel proxy reported enabled but REST/gateway fail with 'fetch failed'), though it includes minor noisy details (specific version and proxy URL) that aren't needed for deduplication." + }, + "usage": { + "input_tokens": 967, + "output_tokens": 944 + } + }, + { + "thread_id": 10106, + "number": 28788, + "kind": "issue", + "title": "Discord REST API ignores proxy configuration", + "summary": { + "problem_summary": "Discord REST API calls from OpenClaw ignore the configured proxy and fail with \"TypeError: fetch failed\" when sending messages, while the Discord WebSocket and other channels (Telegram) work through the proxy and curl against the Discord API via the proxy succeeds.", + "solution_summary": "Modify the Discord channel REST request implementation to use the same proxy-aware SSRF guard/shared fetch wrapper used for web_search/web_fetch so REST calls honor channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY.", + "maintainer_signal_summary": "Reproducible on OpenClaw 2026.2.26 (bc50708) with logs showing \"discord final reply failed: TypeError: fetch failed\"; channels.discord.proxy and environment HTTP_PROXY/HTTPS_PROXY were set and a curl test confirms the proxy itself works; changelog notes web tools were fixed but Discord REST was not included.", + "dedupe_summary": "OpenClaw v2026.2.26: Discord REST API ignores channels.discord.proxy and env HTTP_PROXY/HTTPS_PROXY causing \"TypeError: fetch failed\" for message sends; needs to be routed through the proxy-aware SSRF guard/fetch wrapper introduced for web_search/web_fetch." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Clear, specific summary of the core bug (Discord REST ignoring channel/env proxy causing fetch failures) and suggested fix using the proxy-aware SSRF/fetch wrapper, but it includes a version number which is unnecessary for deduplication." + }, + "usage": { + "input_tokens": 990, + "output_tokens": 803 + } + }, + { + "thread_id": 14855, + "number": 44448, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", + "summary": { + "problem_summary": "openclaw CLI command 'openclaw agent --channel discord --to' delivers to agent:*:main instead of the live Discord channel/thread session because the session resolver does not search by real delivery context (channel+to+accountId) and Discord targets are not normalized (e.g., '148...' vs 'channel:148...'). The issue is present in compiled dist files (dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js).", + "solution_summary": "A manual patch was applied to compiled dist files to add session resolution by delivery target (channel+to+accountId), normalize Discord targets, pass channel and replyAccount/accountId into the resolver, and fix the compact resolver to use buildAgentMainSessionKey and lookup by delivery context so the CLI routes to the correct live session.", + "maintainer_signal_summary": "HIGH regression risk: fixes were made directly in compiled dist files and will be lost on update; maintainers should implement the fix in source, update resolver logic to search by delivery context and normalize Discord targets, and add tests or a post-update revalidation script. Related issues: #36401, #41562, #16149.", + "dedupe_summary": "CLI session resolver bug: 'openclaw agent --channel discord --to' routes to agent:*:main because dist compiled resolvers don't lookup by delivery context (channel+to+accountId) and don't normalize Discord target IDs; patched dist/gateway-cli-*.js, dist/compact-*.js, and dist/register.agent-*.js to add delivery-context lookup and target normalization." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise summary that states the core bug (resolver not using delivery-context and lacking Discord ID normalization), and the compiled files changed, which is the right signal for clustering similar issues." + }, + "usage": { + "input_tokens": 930, + "output_tokens": 941 + } + }, + { + "thread_id": 18179, + "number": 44584, + "kind": "issue", + "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", + "summary": { + "problem_summary": "Two regressions in OpenClaw Discord integration: (1) volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) was being re-injected every turn in thread sessions, causing context bloat and degradation; (2) CLI agent commands (openclaw agent --channel discord --to ...) resolved to the generic agent:*:main session instead of the live thread-bound session.", + "solution_summary": "Patched compiled runtime bundles to stop reinjecting volatile Discord thread metadata and to resolve sessions by actual delivery context. Changes include preventing injection of specific metadata in pi-embedded, adding delivery-target normalization and session lookup in gateway-cli and compact modules, and passing channel/replyAccount to the session resolver in register.agent, plus a fix replacing resolveAgentMainSessionKey with buildAgentMainSessionKey.", + "maintainer_signal_summary": "This is a manual patch applied to compiled files in openclaw/dist (dist/pi-embedded-D6PpOsxP.js, dist/gateway-cli-BjsM6fWb.js, dist/compact-1mmJ_KWL.js, dist/register.agent-DHoJWl4M.js). It requires restarting openclaw-gateway.service and will be lost on update/reinstall, so a re-application script or upstream fix is needed.", + "dedupe_summary": "Fix Discord thread regression that re-injected message_id/reply_to_id/timestamp/Thread starter/Chat history every turn and misrouted CLI agent --channel discord --to ... to agent:*:main by adding delivery-context session resolution (channel+to+accountId) and preventing volatile metadata injection; changes applied to dist/pi-embedded-*.js, dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the two regressions (volatile metadata re-injection and CLI session misrouting) and the implemented fix (delivery-context session resolution and preventing volatile metadata injection), with no template noise." + }, + "usage": { + "input_tokens": 836, + "output_tokens": 808 + } + }, + { + "thread_id": 2538, + "number": 40880, + "kind": "issue", + "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", + "summary": { + "problem_summary": "The sandbox media staging cap is hardcoded as MEDIA_MAX_BYTES = 5 * 1024 * 1024 in dist/store-*.js, causing files larger than 5 MB to be silently rejected or unavailable inside sandboxed agents; there is no way to override this via openclaw.json.", + "solution_summary": "Expose the staging cap as a configurable option (e.g. tools.media.maxBytes) or have the runtime read configuration so MEDIA_MAX_BYTES is not hardcoded; alternatively move or honor per-type maxBytes before staging rejection.", + "maintainer_signal_summary": "Regression present since at least v2026.3.7 and still in v2026.3.8; installed via npm on Ubuntu 24; current workaround is a manual sed patch of dist/store-*.js after npm install; causes large-file uploads to fail silently.", + "dedupe_summary": "Hardcoded MEDIA_MAX_BYTES = 5*1024*1024 in dist/store-*.js enforces a 5 MB sandbox staging limit that cannot be configured via openclaw.json (no tools.media.maxBytes), leading to rejection of files >5MB; request to make MEDIA_MAX_BYTES configurable or defer to existing per-type limits." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise description of a hardcoded 5 MB MEDIA_MAX_BYTES in dist/store-*.js that blocks configuration via openclaw.json and requests making the sandbox staging limit configurable, with no template noise and clear technical terms." + }, + "usage": { + "input_tokens": 883, + "output_tokens": 647 + } + }, + { + "thread_id": 9401, + "number": 27409, + "kind": "issue", + "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", + "summary": { + "problem_summary": "OpenClaw Discord integration cannot send messages: all Discord REST API calls fail with \"fetch failed\" even though REST proxy is configured and logs show \"rest proxy enabled\". The Gateway WebSocket works and bot receives messages, but REST operations (deploying commands, fetching identity, sending replies) error out. Direct curl API calls via the same proxy succeed.", + "solution_summary": "Reporter provides configuration and logs showing rest/gateway proxy enabled and Node/macOS environment; no workaround included beyond demonstrating that curl with the same proxy works. The implied fix is to investigate/fix the REST proxy path or fetch usage in OpenClaw so REST requests honor the configured proxy and succeed.", + "maintainer_signal_summary": "Logs: \"[discord] rest proxy enabled\", \"[discord] gateway proxy enabled\", and repeated \"fetch failed\" / \"TypeError: fetch failed\" during command deployment, identity fetch, and sending replies. OpenClaw version 2026.2.25 on macOS 26.3, Node.js 25.6.1; npm install. Telegram and Gateway WebSocket traffic function correctly while REST fails; curl with -x proxy works.", + "dedupe_summary": "OpenClaw v2026.2.25: Discord REST API requests consistently fail with \"TypeError: fetch failed\" despite rest proxy being configured and logged as enabled; Gateway WebSocket and curl-through-proxy succeed, indicating OpenClaw's REST proxy/fetch integration is broken." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Concise summary that pinpoints OpenClaw's Discord REST requests failing with 'fetch failed' despite proxy enabled and other checks succeeding, but includes an unnecessary version tag which is minor noise." + }, + "usage": { + "input_tokens": 828, + "output_tokens": 540 + } + }, + { + "thread_id": 9156, + "number": 30221, + "kind": "issue", + "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", + "summary": { + "problem_summary": "Discord REST API calls made via @buape/carbon's RequestClient (and some other code paths like memorySearch and attachment downloads) ignore the channels.discord.proxy setting in openclaw.json and call globalThis.fetch without a proxy-aware dispatcher, so they bypass the configured HTTP proxy while the WebSocket gateway correctly uses it.", + "solution_summary": "Have @buape/carbon's REST client accept an optional dispatcher/proxy option (or otherwise use a proxy-aware dispatcher) and update OpenClaw to pass channels.discord.proxy into that client; as a temporary workaround, preload undici's EnvHttpProxyAgent to set the global dispatcher instead of using NODE_OPTIONS=--use-env-proxy which breaks WebSocket connections.", + "maintainer_signal_summary": "Root cause is Node.js 22 native fetch not honoring HTTP(S)_PROXY env vars unless --use-env-proxy is enabled, and carbon's RequestClient calls globalThis.fetch with no dispatcher; enabling --use-env-proxy breaks Discord WebSocket upgrades, so a code change in carbon/OpenClaw is required to propagate proxy config safely. Related: issue #25232 for attachment downloads bypassing proxy.", + "dedupe_summary": "@buape/carbon RequestClient and other REST/fetch paths ignore openclaw.json channels.discord.proxy (they call globalThis.fetch without a proxy dispatcher), causing Discord REST calls and media/embedding fetches to bypass the configured HTTP proxy on Node 22; WebSocket uses the proxy properly, and using --use-env-proxy is not viable because it breaks WebSocket connections." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Clear, focused summary of the core bug (RequestClient and other fetch paths ignore channels.discord.proxy and call globalThis.fetch, bypassing the HTTP proxy) with minor extra detail (Node 22 / --use-env-proxy) that isn't necessary but doesn't harm clustering." + }, + "usage": { + "input_tokens": 837, + "output_tokens": 606 + } + }, + { + "thread_id": 18848, + "number": 44582, + "kind": "issue", + "title": "[Bug]: Performance degradation - context growth + lane blocking under load", + "summary": { + "problem_summary": "OpenClaw gateway degrades under sustained load from long-running conversations: per-session context token growth (writer ~42k tokens, reasoning ~90k) causes increasing per-turn latency, Discord lanes get blocked causing serialization under load, and typing indicators can remain stuck until TTL.", + "solution_summary": "Applied configuration limits: reduced maxConcurrentSessions from 12→6, agents.defaults.maxConcurrent 3→2, subagents.maxConcurrent 8→4, subagents.maxChildrenPerAgent 8→4; set agents.defaults.contextTokens to 80000; increased channels.discord.eventQueue.listenerTimeout from 120000→300000; improved patching script to cover hash variants and restarted the gateway.", + "maintainer_signal_summary": "Gateway restart confirmed active and no new \"lane wait exceeded\" or \"typing TTL reached\" errors; however restart is a mitigation and sustained-load risk remains. Recommendation: make context/concurrency limits adaptive or expose them in openclaw config for easier tuning.", + "dedupe_summary": "Unbounded per-session context token growth (writer ~42k, reasoning ~90k) plus high concurrency saturates lanes and causes lane blocking/serialization and stuck typing indicators; root causes are missing context token limits, excessive concurrency settings, and insufficient event-queue listener timeout." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core issues (unbounded per-session context growth, concurrency saturating lanes causing blocking/serialization, and stuck typing indicators) and their root causes without template noise, so it will cluster with similar issues." + }, + "usage": { + "input_tokens": 771, + "output_tokens": 747 + } + }, + { + "thread_id": 14856, + "number": 44447, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11, Discord thread conversations repeatedly re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, causing context contamination, token bloat and breaking prompt cache behavior. The compiled runtime metadata injector and session handler in dist/ are responsible (missing an isNewSession guard).", + "solution_summary": "A manual patch was applied to compiled files: dist/pi-embedded-D6PpOsxP.js was changed to stop re-injecting message_id, reply_to_id, timestamp, Thread starter and Chat history on each turn; dist/compact-1mmJ_KWL.js was updated to add an isNewSession guard for thread-starter injection and to replace resolveAgentMainSessionKey with buildAgentMainSessionKey. Post-patch validation shows thread context no longer repeats and prompt caching works.", + "maintainer_signal_summary": "HIGH regression risk: changes were made directly in compiled dist/ artifacts and will be lost on updates; maintainers should implement the fix in source, add a regression test and a post-update validation script, and review related issues (#41355, #34228, #37047).", + "dedupe_summary": "Discord thread metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) is being re-injected every turn by the compiled runtime because the session handler lacks an isNewSession guard (files dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js), causing context pollution, token growth and prompt cache busting." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that identifies the exact metadata re-injected, the missing isNewSession guard, affected compiled files, and the concrete impacts, so it will cluster with similar runtime/metadata injection bugs." + }, + "usage": { + "input_tokens": 783, + "output_tokens": 667 + } + }, + { + "thread_id": 14863, + "number": 44449, + "kind": "issue", + "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 Discord thread messages re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, prompt cache busting, token bloat and performance degradation in long threads.", + "solution_summary": "A manual hotfix edited the compiled file dist/pi-embedded-D6PpOsxP.js to stop unconditional per-turn injection and only inject the volatile Discord thread metadata on the first turn; validation shows repeated context stops, prompt caching is restored and token usage falls. Recommended: apply an official fix in source and add a revalidation script to reapply after updates.", + "maintainer_signal_summary": "High regression risk because the change was made in a compiled dist file and will be lost on reinstall/upgrade; related issues (#41355, #34228, #37047) indicate recurrence and the problem has cost/performance impact, so an upstream patch and deployment plan are urgent.", + "dedupe_summary": "OpenClaw v2026.3.11 bug: dist/pi-embedded-D6PpOsxP.js unconditionally re-injects Discord thread metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every message turn; fix is to inject those fields only on the thread's first turn to avoid context pollution and cache busting." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and focused: states the exact bug (unconditional re-injection of Discord thread metadata), lists the affected file and fields, and notes the intended fix to inject only on the first turn, with no template noise." + }, + "usage": { + "input_tokens": 791, + "output_tokens": 737 + } + }, + { + "thread_id": 18847, + "number": 44586, + "kind": "issue", + "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", + "summary": { + "problem_summary": "Two runtime regressions in OpenClaw’s Discord threads: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing unbounded context growth, and the session resolver falling back to agent:*:main instead of selecting a live thread-bound session (routing by channel+to+accountId). These originate from missing patches in compiled runtime bundles (dist/*.js).", + "solution_summary": "Manual patches applied to compiled runtime bundles (dist/reply-BCcP6j4h.js, dist/compact-1mmJ_KWL.js, dist/pi-embedded-Cz5VjpnY.js, dist/gateway-cli-B-E8XzUM.js, dist/register.agent-jS8B5meo.js) to stop metadata re-injection and make the CLI/resolver consistently lookup sessions by delivery context; gateway restarted and validated. Operational hardening added: automatic .bak backups and an idempotent re-application script to restore fixes after updates, plus recommended regression tests.", + "maintainer_signal_summary": "Gateway restarted and is active; backups created (*.bak-rootfix-20260313T023340Z); idempotent reapply script at /home/clawd/scripts/openclaw-rootfix-v2026.3.11.sh; verification checklist passed (no new errors, thread-bound sessions resolve correctly, CLI routing fixed). Note: changes are manual edits to compiled bundles and will be lost on reinstall—reapplication script required on updates.", + "dedupe_summary": "OpenClaw 2026.3.11 Discord-thread runtime bug: compiled dist JS lacked runtime patches so each turn re-injected message/thread metadata causing context bloat, and session resolver didn’t lookup by delivery context (channel+to+accountId) causing fallback to agent:*:main; manually patched dist/*.js, created backups and a reapply script to mitigate but fix must be preserved in source/build to be permanent." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "The summary concisely captures the core technical issues (missing runtime patches causing per-turn metadata re-injection/context bloat and a session resolver lookup bug) and remediation, but includes minor extraneous details (a version and manual mitigation steps) that aren’t needed for deduplication." + }, + "usage": { + "input_tokens": 767, + "output_tokens": 670 + } + }, + { + "thread_id": 5022, + "number": 37344, + "kind": "issue", + "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", + "summary": { + "problem_summary": "OpenClaw 2026.3.2 fails to send Discord reply messages: the bot receives messages and the model generates responses, but sending the reply errors with \"final reply failed: TypeError: fetch failed\" despite Discord proxy being configured.", + "solution_summary": "User reports the Discord proxy is set in openclaw.json and logs show rest/gateway proxy enabled, and curl through the same proxy succeeds; they suspect OpenClaw's Discord REST request for final replies is not applying the configured proxy.", + "maintainer_signal_summary": "Regression (worked before); Node 22.22.0 on Linux via systemd; logs show \"rest proxy enabled\", \"gateway proxy enabled\", \"logged in to discord\" then \"final reply failed: TypeError: fetch failed\"; reproduces in DM and group chats; proxy verified separately with curl.", + "dedupe_summary": "OpenClaw 2026.3.2 Discord outgoing reply failure: \"final reply failed: TypeError: fetch failed\" even though openclaw.json has discord.proxy (http://127.0.0.1:7890) and logs show rest/gateway proxy enabled; proxy works for curl, indicating final reply fetch likely not using configured proxy." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Summary clearly captures the core bug (Discord outgoing replies failing with a fetch error likely due to proxy not being used) but includes minor noise like the specific OpenClaw version and proxy URL that aren't needed for deduplication." + }, + "usage": { + "input_tokens": 707, + "output_tokens": 838 + } + }, + { + "thread_id": 14862, + "number": 44450, + "kind": "issue", + "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", + "summary": { + "problem_summary": "En OpenClaw v2026.3.11 el CLI `openclaw agent --channel discord --to ...` resuelve incorrectamente a la sesión global `agent:*:main` en lugar de la sesión viva del canal/thread Discord objetivo; el resolver de sesiones en el archivo compilado dist/gateway-cli-BjsM6fWb.js no busca por delivery context ni normaliza targets Discord.", + "solution_summary": "Se parcheó manualmente dist/gateway-cli-BjsM6fWb.js para: normalizar targets Discord (p.ej. `148...` → `channel:148...`), resolver sesiones buscando por delivery context compuesto (channel + to + accountId) antes de hacer fallback a `agent:*:main`; validación local confirma enrutamiento correcto.", + "maintainer_signal_summary": "Alto riesgo de regresión: el cambio es sobre archivos compilados en dist/ y se perderá en actualizaciones; requiere implementar la corrección en el código fuente/build pipeline y no solo en dist. Referencias relacionadas: #36401, #41562.", + "dedupe_summary": "CLI `agent --channel discord --to` enruta a agent:*:main porque dist/gateway-cli-BjsM6fWb.js no normaliza targets Discord ni hace lookup por delivery context (channel+to+accountId); manual patch adds normalization and delivery-context session lookup to prevent fallback to agent:*:main." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that identifies the core bug (CLI routes to agent:*:main due to missing Discord target normalization and lack of delivery-context lookup in dist/gateway-cli-BjsM6fWb.js) and notes the applied fix, so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 772, + "output_tokens": 678 + } + }, + { + "thread_id": 14859, + "number": 44453, + "kind": "issue", + "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 a regression in the compiled bundle dist/compact-1mmJ_KWL.js calls a non-existent function resolveAgentMainSessionKey, causing ReferenceError at runtime; the agent command fails and gateways may crash when processing agent commands.", + "solution_summary": "Manually edited dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with the correct buildAgentMainSessionKey(...); this removes the ReferenceError and restores agent/session resolver behavior, but the change is in compiled output and will be overwritten by updates.", + "maintainer_signal_summary": "High-priority regression: fix is currently only in compiled dist file and will be lost on reinstall/upgrades; the source-level code and build output must be corrected and the session-resolver fix (#3) must be applied together with this change to avoid ReferenceError.", + "dedupe_summary": "Regression in v2026.3.11 dist/compact-1mmJ_KWL.js: incorrect use of resolveAgentMainSessionKey instead of buildAgentMainSessionKey caused ReferenceError and agent command failures; temporary manual fix replaces the call in the compiled file but requires a repo/build-level correction to be permanent." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the regression: wrong function used in compiled file causing ReferenceError and temporary local fix, which correctly highlights the code-level change needed for deduplication." + }, + "usage": { + "input_tokens": 743, + "output_tokens": 539 + } + }, + { + "thread_id": 14142, + "number": 42702, + "kind": "issue", + "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", + "summary": { + "problem_summary": "Discord channel plugin's REST API HTTP requests are not using the channels.discord.proxy configuration, causing health checks to return 'fetch failed' and message send operations to time out even though the WebSocket connection uses the proxy and curl via the same proxy works.", + "solution_summary": "Ensure the channels.discord.proxy setting is applied to the Discord REST API HTTP client (in addition to WebSocket connections) so health checks and openclaw message send calls route through the configured proxy and succeed.", + "maintainer_signal_summary": "Regression reported on OpenClaw 2026.3.8 (commit 3caab92) running on Ubuntu 24.04; reproduces reliably where a proxy is required and manifests as failed health checks and message delivery despite external curl via proxy succeeding; labeled bug and regression.", + "dedupe_summary": "discord plugin REST API ignores channels.discord.proxy (only websocket uses it), causing 'fetch failed' health checks and message send failures in proxy-required environments (OpenClaw 2026.3.8)." + }, + "judge": { + "score": 4, + "has_boilerplate": false, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Clear, specific summary of the core bug (REST API ignoring channels.discord.proxy causing fetch failures), but includes an unnecessary version number which is minor noise." + }, + "usage": { + "input_tokens": 699, + "output_tokens": 627 + } + }, + { + "thread_id": 14861, + "number": 44451, + "kind": "issue", + "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", + "summary": { + "problem_summary": "In OpenClaw v2026.3.11 the session resolver inside the agent command in the compiled file dist/compact-1mmJ_KWL.js does not perform a delivery-context lookup (channel+to+accountId), so sessions resolve to agent:*:main instead of the live target channel session and responses are misassociated.", + "solution_summary": "A manual patch was applied to dist/compact-1mmJ_KWL.js adding a lookup for an existing session by delivery context (channel + to + accountId) and only falling back to main if no specific session is found; validation shows routing and response association now correct.", + "maintainer_signal_summary": "High regression risk because the fix edits a compiled file in dist/ and will be overwritten by updates/reinstalls; affected version v2026.3.11—this requires a source-level fix and release to be permanent.", + "dedupe_summary": "dist/compact-1mmJ_KWL.js session resolver missing delivery-context (channel+to+accountId) lookup causes agent command to default to agent:*:main; manual compiled-file patch added delivery-context lookup but needs canonical fix in source and release." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that names the compiled file, the exact missing delivery-context lookup (channel+to+accountId), the wrong fallback behavior (agent:*:main), and notes a manual patch while calling for a source/release fix—suitable for deduplication." + }, + "usage": { + "input_tokens": 713, + "output_tokens": 606 + } + }, + { + "thread_id": 21902, + "number": 55855, + "kind": "issue", + "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", + "summary": { + "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English, causing usability barriers for non-English users (settings, buttons, agent/skill pages, channel config, status messages, tooltips).", + "solution_summary": "Extract hardcoded strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, provide a language selector or auto-detect system language, and build infrastructure to support additional locales going forward.", + "maintainer_signal_summary": "Requester notes docs already have zh-CN translations, points out hardcoded strings in the bundled control-ui assets, marks priority Medium-High for UX/accessibility, and offers to hear about roadmap or contribute to implementation.", + "dedupe_summary": "Feature request to add Simplified Chinese (zh-CN) i18n to the OpenClaw Control UI by extracting hardcoded English strings into locale files, adding zh-CN translations, and adding a language selector/auto-detection so the UI supports multiple languages." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core feature request—adding zh-CN i18n to OpenClaw Control UI by extracting hardcoded strings into locale files and adding language selection/auto-detection—without extraneous template text." + }, + "usage": { + "input_tokens": 634, + "output_tokens": 441 + } + }, + { + "thread_id": 22366, + "number": 55328, + "kind": "pull_request", + "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", + "summary": { + "problem_summary": "WhatsApp group chats could not apply channel-configured systemPrompt instructions at account or group granularity, preventing per-account and per-group prompt customization (including wildcard defaults). The issue affected the gateway/integration configuration/schema and runtime prompt injection for WhatsApp groups.", + "solution_summary": "Adds systemPrompt support to channels.whatsapp config types/schema and implements account-level resolution and group prompt composition; the final injected prompt is the account prompt plus the resolved group prompt (joined with a blank line). The PR includes tests and docs and limits changes to WhatsApp gateway/config handling.", + "maintainer_signal_summary": "Backwards-compatible feature change touching gateway/orchestration, integrations and API/contracts; includes tests and documentation, does not change non-WhatsApp channels, hot-reload behavior, or group allowlist/mention-gating semantics, and closes the linked issue.", + "dedupe_summary": "Extend WhatsApp config to support systemPrompt at channels.whatsapp, channels.whatsapp.accounts., and channels.whatsapp(.accounts).groups. with resolution: account systemPrompt overrides root systemPrompt; group resolution prefers specific group then '*' wildcard within the active groups map; account-level groups fully replace root groups; final prompt = account_prompt + blank line + resolved_group_prompt." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of adding multi-level WhatsApp systemPrompt support and the precise resolution rules, which is ideal for deduplication." + }, + "usage": { + "input_tokens": 2410, + "output_tokens": 633 + } + }, + { + "thread_id": 17692, + "number": 45354, + "kind": "pull_request", + "title": "Fix WhatsApp group append processing and account-scoped gatin", + "summary": { + "problem_summary": "WhatsApp group messages received via messages.upsert(type=\"append\") were being skipped before entering the inbound/agent path, and WhatsApp group policy resolution could ignore account-scoped configuration (accountId), causing account-scoped groupAllowFrom/allowFrom to be overlooked and allowlist gating to fail closed.", + "solution_summary": "Append processing was enabled only for WhatsApp group messages (DM append history still skipped) and group policy resolution was changed to propagate accountId and use account-scoped groupAllowFrom/allowFrom fallback when resolving WhatsApp group gating defaults.", + "maintainer_signal_summary": "Bug fix limited to WhatsApp integration; no config/schema changes. Touches src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts; verified against live deployment and targeted tests. Risk: group append may process backlog messages (mitigated by scoping to groups and keeping DM append skipped).", + "dedupe_summary": "Fix: WhatsApp group messages delivered as messages.upsert(type=\"append\") were dropped and account-scoped WhatsApp group policy ignored; change scopes append processing to groups and propagates accountId so account-scoped groupAllowFrom/allowFrom are applied during group gating." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core bug (append-typed group messages dropped) and the fix (limit append processing to groups and use accountId so account-scoped groupAllowFrom/allowFrom are honored), with no template noise." + }, + "usage": { + "input_tokens": 1962, + "output_tokens": 715 + } + }, + { + "thread_id": 20932, + "number": 52512, + "kind": "pull_request", + "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", + "summary": { + "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because the gateway mis-identified the request origin: resolveClientIp returned undefined when a trusted proxy connected without X-Forwarded-* headers, and isLocalDirectRequest did not fall back to the socket remoteAddress when XFF resolved to a non-loopback Tailscale IP.", + "solution_summary": "Make resolveClientIp return the socket address as a fallback for direct trusted-proxy requests; update isLocalDirectRequest to consult req.socket.remoteAddress when auth.allowRealIpFallback is enabled; gate .ts.net host acceptance behind the existing auth.allowTailscale flag and thread that flag through callers; update docs and add tests.", + "maintainer_signal_summary": "Closes #28472 and depends on #50636; tests added/updated (src/gateway/auth.test.ts, net.test.ts) and passing; no new config keys introduced but docs require gateway.trustedProxies and gateway.allowRealIpFallback when using Tailscale Serve; small behavioral risk limited to trusted-proxy no-XFF cases.", + "dedupe_summary": "Fix 401 by having resolveClientIp fall back to req.socket.remoteAddress for trusted proxies that omit forwarding headers, and by changing isLocalDirectRequest to use socket remoteAddress when allowRealIpFallback is true; additionally gate .ts.net host-header acceptance behind auth.allowTailscale and thread that flag through gateway auth/net/server components." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that captures the core code changes (resolveClientIp and isLocalDirectRequest fallbacks, .ts.net gating, and threading auth.allowTailscale) without boilerplate or irrelevant details." + }, + "usage": { + "input_tokens": 1801, + "output_tokens": 796 + } + }, + { + "thread_id": 13791, + "number": 42906, + "kind": "pull_request", + "title": "fix(security): block non-self messages in WhatsApp self-chat mode", + "summary": { + "problem_summary": "In WhatsApp self-chat deployments, checkInboundAccessControl() computed an isSelfChat heuristic but never used it as a gate, so non-self messages (group messages, DMs from strangers, broadcasts) could pass through because resolveWhatsAppRuntimeGroupPolicy() defaults to \"open\" when groupPolicy is unspecified. This violates the intended self-chat isolation boundary in the inbound access-control path.", + "solution_summary": "Added an early-return guard in src/web/inbound/access-control.ts: when account.selfChatMode === true, non-self messages are immediately rejected before any group/DM policy evaluation, preserving self-messages via isSamePhone and retaining the isSelfChat value for logging/UI while avoiding heuristic-based blocking.", + "maintainer_signal_summary": "Change is scoped to access-control.ts for WhatsApp inbound processing; it intentionally only enforces explicit selfChatMode (auto-detected heuristic is not hard-blocked) and preserves existing group/DM policy resolution for other cases. All relevant tests pass and the guard can be reverted by removing the early-return block.", + "dedupe_summary": "Fix: enforce explicit WhatsApp self-chat isolation by adding an early-return in checkInboundAccessControl to block all non-self messages when account.selfChatMode === true; addresses a bug where isSelfChat was computed but not enforced and groupPolicy defaulted to \"open\", allowing unauthorized non-self messages through." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that clearly states the bug and the fix, references the relevant functions/config (checkInboundAccessControl, account.selfChatMode, groupPolicy), and contains no template boilerplate." + }, + "usage": { + "input_tokens": 1516, + "output_tokens": 655 + } + }, + { + "thread_id": 4208, + "number": 4402, + "kind": "pull_request", + "title": "fix: store group messages from non-allowlisted senders as pending context", + "summary": { + "problem_summary": "With groupPolicy set to \"allowlist\", group messages from senders not in groupAllowFrom are currently dropped at the access-control layer and never reach the mention-gating/context-storage logic, so the agent cannot see non-allowlisted members' messages as pending context.", + "solution_summary": "Introduce an opt-in flag groupContextFromAll (default false) that causes access-control to return storeForContext for non-allowlisted group senders; the inbound pipeline marks those messages as contextOnly (no read receipts, no reply triggered) and records them in group pending history. Added config/schema/types changes and threaded the new outcome through monitor/on-message to store context-only messages.", + "maintainer_signal_summary": "Backwards compatible (default false), opt-in per-account or top-level config, tested locally; changes are localized to WhatsApp inbound pipeline and config but Greptile flagged potential edge cases around phone-number normalization/allowlist matching and bypassing some group gating/state updates. Review should focus on allowlist normalization, interactions with applyGroupGating, and the new storeForContext/contextOnly handling across access-control and monitors.", + "dedupe_summary": "PR adds groupContextFromAll flag for WhatsApp to change behavior when groupPolicy: \"allowlist\": instead of dropping messages from non-allowlisted senders, access-control returns storeForContext and messages are marked contextOnly and stored in group pending history (no replies/read receipts); key files: src/web/inbound/access-control.ts, monitor.ts, on-message.ts, types/config schema." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: describes the new WhatsApp flag, the exact behavior change for groupPolicy: \"allowlist\" (store for context instead of drop, contextOnly, pending history), and thus will match similar issues; file paths are minor extra detail." + }, + "usage": { + "input_tokens": 1533, + "output_tokens": 590 + } + }, + { + "thread_id": 9553, + "number": 20072, + "kind": "pull_request", + "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", + "summary": { + "problem_summary": "sessions_spawn always created a new random-UUID session for sub-agents, preventing reuse of conversation history and enabling a session-key workflow that could bypass allowlist checks.", + "solution_summary": "Add an optional sessionKey parameter to sessions_spawn so sub-agents can run in deterministic sessions: short keys are namespaced to agent:{agentId}:subagent:{key}, fully-qualified keys are allowed but validated to ensure the embedded agentId matches targetAgentId, and omission falls back to crypto.randomUUID(); includes four tests and fixes allowlist bypass.", + "maintainer_signal_summary": "Blocking issues noted: a test imports a non-existent module (openclaw-tools.js vs clawdbot-tools.js) causing compile failure; cross-agent validation has a case-sensitivity bug (embedded agentId not normalized) causing false mismatches; inconsistent error handling (throw vs jsonResult) in security checks needs harmonization.", + "dedupe_summary": "Introduce sessionKey param to sessions_spawn to enable deterministic reuse of sub-agent sessions (namespacing short keys as agent:{agentId}:subagent:{key}, accepting fully-qualified keys only if their embedded agentId matches targetAgentId) and fall back to random UUID; addresses conversation-history reuse and allowlist bypass, but PR currently has a broken test import and case-sensitivity/error-handling validation bugs." + }, + "judge": { + "score": 4, + "has_boilerplate": true, + "signal_density": 4, + "would_cluster_correctly": true, + "reasoning": "Good, specific summary of the new sessionKey behavior and validation rules that will match similar issues, but it includes unnecessary test/import/bug notes that should be removed for deduplication." + }, + "usage": { + "input_tokens": 1586, + "output_tokens": 804 + } + }, + { + "thread_id": 8969, + "number": 21898, + "kind": "pull_request", + "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", + "summary": { + "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) fails because the adapter calls editMessageText on caption-only media; Telegram responds 400 'there is no text in the message to edit', causing edits to fail and UI inconsistency.", + "solution_summary": "Updated src/telegram/send.ts editMessageTelegram to catch the specific 400 error (NO_TEXT_IN_MESSAGE_RE) and retry using editMessageCaption for captionable media; added a direct editMessageReplyMarkup path for buttons-only updates, preserved HTML parse fallback and MESSAGE_NOT_MODIFIED handling, and added six unit tests covering the paths.", + "maintainer_signal_summary": "Bug-fix PR scoped to the Telegram channel adapter only, backward compatible with no interface changes, adds two Telegram API methods and six tests; low risk but regex matching the exact Telegram error string is a potential fragility.", + "dedupe_summary": "In src/telegram/send.ts, editMessageTelegram now auto-detects captionable media by catching Telegram's 400 'there is no text in the message to edit' (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption; buttons-only updates use editMessageReplyMarkup; includes tests and preserves existing parse-mode and MESSAGE_NOT_MODIFIED behavior." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that pinpoints the exact code change and behavior (auto-retry to editMessageCaption on Telegram 400 for captionable media) with minimal noise, so it will cluster correctly with similar issues." + }, + "usage": { + "input_tokens": 1467, + "output_tokens": 725 + } + }, + { + "thread_id": 17568, + "number": 47391, + "kind": "pull_request", + "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", + "summary": { + "problem_summary": "PR #24962 hardcoded disableBlockStreaming: true for the WhatsApp integration, which disabled block streaming entirely and caused all messages to be batched and sent only when the agent finished, ignoring per-account channels.whatsapp.blockStreaming and agents.defaults.blockStreamingDefault settings.", + "solution_summary": "Restore config-driven behavior: make disableBlockStreaming derive from the per-account channels.whatsapp.blockStreaming (falling back to agents.defaults.blockStreamingDefault) and change the deliver callback to suppress only tool payloads (reasoning payloads remain filtered upstream). Tests and live WhatsApp verification confirm progressive delivery at text boundaries.", + "maintainer_signal_summary": "Behavior is backward compatible; reasoning/thinking content is still suppressed upstream by shouldSuppressReasoningPayload; to disable this change quickly set channels.whatsapp.blockStreaming: false or remove the global default; watch for regressions if the upstream suppression filter is altered.", + "dedupe_summary": "WhatsApp integration bug: revert hardcoded disableBlockStreaming=true introduced in PR #24962 by making block streaming config-driven (channels.whatsapp.blockStreaming or agents.defaults.blockStreamingDefault) and update deliver callback to only suppress tool payloads while relying on shouldSuppressReasoningPayload upstream to block reasoning content." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change (undo hardcoded disableBlockStreaming, make it config-driven, and restrict deliver callback suppression to tool payloads) without boilerplate, so it would cluster with similar WhatsApp block-streaming fixes." + }, + "usage": { + "input_tokens": 1459, + "output_tokens": 575 + } + }, + { + "thread_id": 4129, + "number": 38337, + "kind": "pull_request", + "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", + "summary": { + "problem_summary": "Sending video/media (>5MB) to the Telegram channel caused the gateway to hang indefinitely and become unresponsive because fetchRemoteMedia threw a content-length > maxBytes error and the dispatcher cleanup (Agent.close) deadlocked when the response body was left unconsumed.", + "solution_summary": "Increase default channels.telegram.mediaMaxMb from 5 to 20; fix closeDispatcher to avoid undici Agent.close deadlock by adding a timeout and fallback destroy; make media download errors non-fatal so captions/text still forward; add timeouts and retries to getFile and download flows; strip inbound media-path metadata from UI.", + "maintainer_signal_summary": "Backward-compatible default change (5→20MB configurable via channels.telegram.mediaMaxMb); verified end-to-end with a 7.5MB video and tests (strip-inbound-meta test added); no new permissions or dependencies; risk: 3s closeDispatcher timeout could trigger on slow drains but falls back to destroy and can be reverted in code.", + "dedupe_summary": "Fix Telegram gateway hang caused by undici Agent.close deadlock when response body is unconsumed by: raising telegram mediaMaxMb default to 20, making media download errors non-fatal, adding timeouts/retries to getFile/download, and changing closeDispatcher to use a timeout+destroy fallback." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, focused summary that names the root cause (undici Agent.close deadlock), the effect (Telegram gateway hang), and the specific fixes (increase mediaMaxMb, non-fatal media errors, timeouts/retries, timeout+destroy fallback) so it will match similar issues precisely." + }, + "usage": { + "input_tokens": 1555, + "output_tokens": 640 + } + }, + { + "thread_id": 21735, + "number": 55953, + "kind": "pull_request", + "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", + "summary": { + "problem_summary": "When a Telegram user sends a document (PDF, DOCX, etc.) with no caption, the inbound body text fallback in the Telegram integration always used the hardcoded \"\", causing the agent to misidentify documents as images in resolveTelegramInboundBody.", + "solution_summary": "The fallback now uses the already-computed, type-aware placeholder variable so media placeholders resolve to the correct form (e.g. \"\", \"\", etc.) and unit tests were added to verify document placeholder behavior and getFile handling.", + "maintainer_signal_summary": "Change is scoped to agent-facing body text placeholder only; media download, extraction, and understanding pipelines are unchanged; new unit tests were added in extensions/telegram/src/bot/helpers.test.ts and extensions/telegram/src/bot.create-telegram-bot.test.ts; this closes #7116 and is backward compatible with low risk.", + "dedupe_summary": "Fix: replace hardcoded \"\" in resolveTelegramInboundBody fallback with the computed type-aware placeholder so non-audio Telegram attachments (e.g. documents) are labeled \"\"; unit tests added to lock in behavior." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the bug and fix naming the function and replacement placeholder (with an example) and only minor mention of tests, so it will cluster correctly with similar Telegram media-placeholder fixes." + }, + "usage": { + "input_tokens": 1382, + "output_tokens": 604 + } + }, + { + "thread_id": 2463, + "number": 40950, + "kind": "pull_request", + "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", + "summary": { + "problem_summary": "Media staging limit is hardcoded to MEDIA_MAX_BYTES (5 MB) in src/media/store.ts, so files larger than 5 MB silently fail to stage into sandboxed agents and users cannot override the cap via config.", + "solution_summary": "Adds a new media.maxBytes config field (optional, max 500 MB), replaces the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(), has the gateway read and apply the config on boot, preserves default 5 MB when unset, and updates error messages to reflect the configured limit; tests updated and passing.", + "maintainer_signal_summary": "All relevant tests pass (56), type-check and lint clean; change is backward-compatible (default preserved, deprecated constant still exported), migration optional, schema caps value at 500 MB to mitigate disk-fill risk, and the PR closes #40880; reviewers’ bot comments were addressed.", + "dedupe_summary": "Make MEDIA_MAX_BYTES configurable: replace hardcoded 5MB in src/media/store.ts with get/set API driven by new media.maxBytes config applied at gateway boot (optional, default 5MB, schema max 500MB), and update error messaging to show the configured MB limit." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change (replace hardcoded 5MB with configurable media.maxBytes, applied at gateway boot, and updated error messaging) without boilerplate or irrelevant details." + }, + "usage": { + "input_tokens": 1503, + "output_tokens": 582 + } + }, + { + "thread_id": 5418, + "number": 30024, + "kind": "pull_request", + "title": "fix: pass file metadata to agent when Telegram media download fails", + "summary": { + "problem_summary": "Telegram integration: when a file/photo download failed (due to exceeding configured mediaMaxMb or Telegram API errors), the handler returned early and the entire message was silently dropped so the agent never saw the message, sent no ack, and the user got no indication.", + "solution_summary": "On download failure the code now extracts file metadata (name, size, type) from the Telegram message object, builds a metadata stub injected into msg.text explaining why the file couldn't be downloaded, and allows the message to continue through the normal pipeline so the agent can handle/respond.", + "maintainer_signal_summary": "Bug fix in src/telegram/bot-handlers.ts affecting integrations; backward compatible, no config changes, removes prior automatic \"file too large/failed to download\" bot replies so agent decides response; revertable by reverting the single commit; risk limited to error-path mutation of msg.text.", + "dedupe_summary": "Fix for Telegram handler early-return on media download failure: instead of returning on size/API errors (e.g., >mediaMaxMb default 5MB or Telegram 20MB getFile limit), build a file-metadata stub (name,size,type,reason) and append to msg.text then continue to processMessage so files aren't silently dropped." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and focused: it states the bug (early-return on Telegram media download failure causing silent drops) and the exact fix (build/append file-metadata stub and continue processing), so it will correctly cluster similar reports." + }, + "usage": { + "input_tokens": 1345, + "output_tokens": 648 + } + }, + { + "thread_id": 5796, + "number": 24117, + "kind": "pull_request", + "title": "feat(cli): add --session-key targeting for openclaw agent", + "summary": { + "problem_summary": "openclaw agent CLI lacked a --session-key option, causing CLI-triggered deliveries to resolve to the agent main session when external automation had a stable session key but not a UUID sessionId (making callback routing unreliable).", + "solution_summary": "Adds a --session-key CLI flag in src/cli/program/register.agent.ts, extends AgentCliOpts with sessionKey and forwards it into resolveSessionKeyForRequest via src/commands/agent-via-gateway.ts, updates help/validation text, and adds tests for parsing and gateway forwarding.", + "maintainer_signal_summary": "Includes unit tests (register.agent.test.ts and agent-via-gateway.test.ts) that pass locally; change is additive/opt-in, backward compatible, and presented as low-risk.", + "dedupe_summary": "Add --session-key flag to openclaw agent CLI to target stable session keys (e.g., agent:demo:slack:channel:...), forward sessionKey into resolveSessionKeyForRequest and gateway calls, preserve resolution precedence (sessionKey > sessionId > to > agent), and update help/validation with tests." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that highlights the new --session-key CLI flag, where it's propagated (resolveSessionKeyForRequest/gateway), and the preserved resolution precedence, which is sufficient for deduplication." + }, + "usage": { + "input_tokens": 1317, + "output_tokens": 809 + } + }, + { + "thread_id": 766, + "number": 11611, + "kind": "pull_request", + "title": "feat: separate group-level allowlist from sender-level command authorization", + "summary": { + "problem_summary": "groupPolicy: \"allowlist\" currently uses groupAllowFrom (sender phone numbers) to gate all group interactions, so you cannot allow specific group JIDs while letting any participant chat, nor separate slash-command authorization from group-level acceptance, and spam groups cannot be blocked by JID.", + "solution_summary": "When groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access control now gates by group JID (the groups mapping) instead of sender phone number; groupAllowFrom remains the gate for slash commands, anyone in an approved group can chat, and when no groups mapping exists the legacy sender-based behavior is preserved. The change updates src/web/inbound/access-control.ts, docs in src/config/types.whatsapp.ts, and adds four tests covering the new behavior (depends on #3326).", + "maintainer_signal_summary": "Localized change to checkInboundAccessControl delegating to resolveChannelGroupPolicy; backward-compatible fallback preserved; JSDoc updated and four Vitest cases added; one minor outdated inline comment noted but logic appears safe.", + "dedupe_summary": "Change WhatsApp group allowlist behavior so that when channels.whatsapp.groups (group JID mapping) is present and groupPolicy=\"allowlist\", inbound group access is determined by group JIDs (allowlisted groups) while groupAllowFrom continues to control slash-command authorization; if no groups mapping exists, fall back to sender-based filtering." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it captures the change to use channels.whatsapp.groups (group JIDs) as the allowlist when groupPolicy=\"allowlist\", preserves groupAllowFrom for command auth, and notes the sender-based fallback." + }, + "usage": { + "input_tokens": 1367, + "output_tokens": 672 + } + }, + { + "thread_id": 17924, + "number": 46425, + "kind": "pull_request", + "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", + "summary": { + "problem_summary": "Telegram account setting mediaMaxMb (mediaMaxBytes) was not being applied for outbound reply messages; loadWebMedia in the reply delivery path received no maxBytes and fell back to the 100 MB default, so replies could exceed operator-configured limits.", + "solution_summary": "Thread mediaMaxBytes through the Telegram outbound delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia receives the configured maxBytes; added unit tests verifying behavior and preserved behavior when unset.", + "maintainer_signal_summary": "This is a small bugfix touching Telegram integration; changes are backward compatible and limited to parameter threading, with two unit tests added and 108 related tests passing; no defaults or media internals changed and the direct-send path was already correct.", + "dedupe_summary": "Thread mediaMaxBytes parameter to loadWebMedia for Telegram outbound reply delivery (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts and bot-native-commands.ts) so mediaMaxMb configured on telegramAccounts is enforced instead of falling back to the 100MB default." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core fix (threading mediaMaxBytes through listed files to enforce telegramAccounts' mediaMaxMb instead of the 100MB default) with no boilerplate or noise." + }, + "usage": { + "input_tokens": 1368, + "output_tokens": 437 + } + }, + { + "thread_id": 5712, + "number": 37050, + "kind": "pull_request", + "title": "fix(telegram): add HTML parse fallback for media captions", + "summary": { + "problem_summary": "Media sends with captions containing unmatched or invalid HTML entities caused Telegram API 400 \"can't parse entities\" errors and the entire media send would fail because only text messages had an HTML-parse fallback; the media send path lacked retry logic.", + "solution_summary": "Wrap the media send callback in withTelegramHtmlParseFallback so that on an HTML parse error the media send is retried without parse_mode and with the raw caption; this change covers photo, document, video, animation, audio, voice and video note sends and includes a unit test verifying the retry.", + "maintainer_signal_summary": "Change is limited to the media send inner callback in src/telegram/send.ts; text message, edit message, thread fallback and caption splitting logic are unchanged. A unit test was added to confirm the first call uses parse_mode: 'HTML' and the retry omits parse_mode and uses the raw caption; backwards compatible and small scope.", + "dedupe_summary": "Add HTML parse fallback for Telegram media captions by wrapping media send callbacks (sendPhoto, sendDocument, sendVideo, sendAnimation, sendAudio, sendVoice, video_note) in withTelegramHtmlParseFallback so parse errors (\"can't parse entities\") are retried without parse_mode using the raw caption (implemented in src/telegram/send.ts)." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: describes adding an HTML-parse fallback for Telegram media captions by wrapping media send functions in withTelegramHtmlParseFallback and retrying without parse_mode using the raw caption, which is exactly the core change." + }, + "usage": { + "input_tokens": 1242, + "output_tokens": 828 + } + }, + { + "thread_id": 21769, + "number": 55280, + "kind": "pull_request", + "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", + "summary": { + "problem_summary": "WhatsApp had block streaming effectively disabled/hardcoded which batched intermediate text and introduced a delivery race in the block-reply pipeline, sometimes causing neither intermediate blocks nor the final reply to be delivered.", + "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming (per-account/global), fix the boolean handling so the setting actually activates, add coalescing defaults, implement dispatcher.sendBlockReplyAsync and await block delivery in onBlockReply so sentContentKeys are marked only after confirmed delivery, and update tests.", + "maintainer_signal_summary": "Existing tests were updated and new cases added; manual verification shows streaming-off yields a single final reply and streaming-on delivers incremental blocks. This is a root-cause fix (not a revert) addressing the optimistic-enqueue race by awaiting confirmed delivery.", + "dedupe_summary": "Enable configurable WhatsApp block streaming and fix delivery race by replacing optimistic dispatcher.sendBlockReply() enqueue-without-await with sendBlockReplyAsync awaited in onBlockReply, correct disableBlockStreaming boolean logic so blockStreaming actually activates, add blockStreamingCoalesceDefaults, and adjust dispatch/pipeline to mark sentContentKeys only after confirmed delivery." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, technical summary that captures the core change—making WhatsApp block streaming configurable and fixing the delivery race by awaiting block-reply sends and correcting the boolean logic—without boilerplate, so it will cluster with similar fixes." + }, + "usage": { + "input_tokens": 1155, + "output_tokens": 611 + } + }, + { + "thread_id": 8098, + "number": 25543, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked group-level allowlist support — only sender-level groupAllowFrom existed, so specific Signal groups could not be explicitly allowed to bypass sender checks or run control commands.", + "solution_summary": "Introduces channels.signal.groups. config with a new SignalGroupConfig type and Zod schema, wires resolveChannelGroupPolicy into the Signal event handler and command authorization so configured groups bypass sender-level checks, and updates docs.", + "maintainer_signal_summary": "TypeScript compilation passes and manual Signal testing performed; implementation follows existing patterns from other channels, preserves compatibility with groupAllowFrom, and integrates group-level allow into commandGate; minimal-risk change with docs added.", + "dedupe_summary": "Add group-level allowlist for Signal via channels.signal.groups. config (SignalGroupConfig + Zod schema) that lets listed groups bypass sender-level groupAllowFrom and be authorized for control commands by wiring resolveChannelGroupPolicy into event handling and command authorization." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary of the core change (Signal group-level allowlist config, schema and event/authorization wiring) without boilerplate, so it will cluster with similar Signal group-allowlist/authorization changes." + }, + "usage": { + "input_tokens": 1097, + "output_tokens": 491 + } + }, + { + "thread_id": 539, + "number": 29154, + "kind": "pull_request", + "title": "fix(signal): add group-level allowlist support via groups config", + "summary": { + "problem_summary": "Signal channel lacked group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter (sender phone/UUID never matches a group ID).", + "solution_summary": "Adds a groups config to SignalAccountSchemaBase and invokes resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom; adds four tests to cover group-policy resolution and includes an example config.", + "maintainer_signal_summary": "Schema change: adds channels.signal.groups (requireMention, tools, toolsBySender). Event handler change: insert resolveChannelGroupPolicy call for Signal. Tests: +4 tests, all existing config/group-policy/monitor tests pass; fixes #25540.", + "dedupe_summary": "Add group-level allowlist support for Signal by adding channels.signal.groups to the schema and calling resolveChannelGroupPolicy in Signal's access check so group IDs in groupPolicy: \"allowlist\" are recognized instead of being misinterpreted by sender-level groupAllowFrom." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise and specific: it captures the missing Signal group-level allowlist, explains the root cause (group IDs misinterpreted by sender-level groupAllowFrom), and states the code changes (add channels.signal.groups and call resolveChannelGroupPolicy), so it will cluster correctly with similar fixes." + }, + "usage": { + "input_tokens": 897, + "output_tokens": 620 + } + }, + { + "thread_id": 5565, + "number": 28178, + "kind": "pull_request", + "title": "feat(whatsapp): support config-driven block streaming", + "summary": { + "problem_summary": "WhatsApp messages are currently coalesced into one final payload when an agent sends multiple messages with tool calls/sleep between them because process-message.ts hardcodes disableBlockStreaming: true and uses queuedFinal to mark delivery, preventing intermediate blocks from being sent as separate replies.", + "solution_summary": "This PR makes WhatsApp respect the channels.whatsapp.blockStreaming config by adding resolveWhatsAppBlockStreamingEnabled(), replacing the hardcoded disableBlockStreaming, fixing the queuedFinal/delivery logic so intermediate blocks count as replies when blockStreaming is enabled, registering blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in the WhatsApp dock, and updating tests.", + "maintainer_signal_summary": "Includes code changes in process-message.ts and dock.ts, updated inbound contract tests to cover enabled/disabled/default behaviors, all 10 existing tests pass; labels: channel: whatsapp-web, app: web-ui, agents, size: S.", + "dedupe_summary": "Make WhatsApp channel respect config-driven blockStreaming (channels.whatsapp.blockStreaming) instead of hardcoded disableBlockStreaming:true; fix delivery logic so intermediate block payloads are treated as replies; register blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE; update tests." + }, + "judge": { + "score": 5, + "has_boilerplate": false, + "signal_density": 5, + "would_cluster_correctly": true, + "reasoning": "Concise, specific summary that captures the core change (config-driven blockStreaming for WhatsApp), the delivery-fix for intermediate block payloads, and capability registration without boilerplate or irrelevant details." + }, + "usage": { + "input_tokens": 647, + "output_tokens": 573 + } + } + ] +} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/test-threads.yaml b/.context/compound-engineering/ce-optimize/summary-prompt/test-threads.yaml new file mode 100644 index 0000000..6e194d7 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/summary-prompt/test-threads.yaml @@ -0,0 +1,92 @@ +spec_name: summary-prompt +description: > + Optimize the system prompt for summarizing GitHub issues/PRs. + Goal: extract signal, strip template boilerplate, remove testing notes. + Judge: LLM rates summary quality for deduplication clustering. + +test_threads: + issues: + - { thread_id: 15126, number: 49990, cluster_id: 245465, cluster_size: 10, body_length: 7604 } + - { thread_id: 8920, number: 30244, cluster_id: 245465, cluster_size: 10, body_length: 3390 } + - { thread_id: 19616, number: 51018, cluster_id: 245465, cluster_size: 10, body_length: 3358 } + - { thread_id: 16324, number: 47670, cluster_id: 245465, cluster_size: 10, body_length: 3025 } + - { thread_id: 10106, number: 28788, cluster_id: 245465, cluster_size: 10, body_length: 2772 } + - { thread_id: 14855, number: 44448, cluster_id: 245467, cluster_size: 10, body_length: 2554 } + - { thread_id: 18179, number: 44584, cluster_id: 245467, cluster_size: 10, body_length: 2413 } + - { thread_id: 2538, number: 40880, cluster_id: 245464, cluster_size: 10, body_length: 2392 } + - { thread_id: 9401, number: 27409, cluster_id: 245465, cluster_size: 10, body_length: 2275 } + - { thread_id: 9156, number: 30221, cluster_id: 245465, cluster_size: 10, body_length: 2249 } + - { thread_id: 18848, number: 44582, cluster_id: 245467, cluster_size: 10, body_length: 2143 } + - { thread_id: 14856, number: 44447, cluster_id: 245467, cluster_size: 10, body_length: 2126 } + - { thread_id: 14863, number: 44449, cluster_id: 245467, cluster_size: 10, body_length: 2038 } + - { thread_id: 18847, number: 44586, cluster_id: 245467, cluster_size: 10, body_length: 2000 } + - { thread_id: 5022, number: 37344, cluster_id: 245465, cluster_size: 10, body_length: 1891 } + - { thread_id: 14862, number: 44450, cluster_id: 245467, cluster_size: 10, body_length: 1850 } + - { thread_id: 14859, number: 44453, cluster_id: 245467, cluster_size: 10, body_length: 1780 } + - { thread_id: 14142, number: 42702, cluster_id: 245465, cluster_size: 10, body_length: 1776 } + - { thread_id: 14861, number: 44451, cluster_id: 245467, cluster_size: 10, body_length: 1703 } + - { thread_id: 21902, number: 55855, cluster_id: 245466, cluster_size: 10, body_length: 1679 } + + pull_requests: + - { thread_id: 22366, number: 55328, cluster_id: 245463, cluster_size: 10, body_length: 10165 } + - { thread_id: 17692, number: 45354, cluster_id: 245463, cluster_size: 10, body_length: 7728 } + - { thread_id: 20932, number: 52512, cluster_id: 245462, cluster_size: 11, body_length: 6111 } + - { thread_id: 13791, number: 42906, cluster_id: 245463, cluster_size: 10, body_length: 5610 } + - { thread_id: 4208, number: 4402, cluster_id: 245463, cluster_size: 10, body_length: 5532 } + - { thread_id: 9553, number: 20072, cluster_id: 245468, cluster_size: 9, body_length: 5490 } + - { thread_id: 8969, number: 21898, cluster_id: 245464, cluster_size: 10, body_length: 5434 } + - { thread_id: 17568, number: 47391, cluster_id: 245463, cluster_size: 10, body_length: 5429 } + - { thread_id: 4129, number: 38337, cluster_id: 245464, cluster_size: 10, body_length: 5407 } + - { thread_id: 21735, number: 55953, cluster_id: 245464, cluster_size: 10, body_length: 5162 } + - { thread_id: 2463, number: 40950, cluster_id: 245464, cluster_size: 10, body_length: 4839 } + - { thread_id: 5418, number: 30024, cluster_id: 245464, cluster_size: 10, body_length: 4622 } + - { thread_id: 5796, number: 24117, cluster_id: 245468, cluster_size: 9, body_length: 4539 } + - { thread_id: 766, number: 11611, cluster_id: 245463, cluster_size: 10, body_length: 4483 } + - { thread_id: 17924, number: 46425, cluster_id: 245464, cluster_size: 10, body_length: 4429 } + - { thread_id: 5712, number: 37050, cluster_id: 245464, cluster_size: 10, body_length: 4387 } + - { thread_id: 21769, number: 55280, cluster_id: 245463, cluster_size: 10, body_length: 3817 } + - { thread_id: 8098, number: 25543, cluster_id: 245463, cluster_size: 10, body_length: 3514 } + - { thread_id: 539, number: 29154, cluster_id: 245463, cluster_size: 10, body_length: 2620 } + - { thread_id: 5565, number: 28178, cluster_id: 245463, cluster_size: 10, body_length: 1698 } + +thread_ids_flat: + - 15126 + - 8920 + - 19616 + - 16324 + - 10106 + - 14855 + - 18179 + - 2538 + - 9401 + - 9156 + - 18848 + - 14856 + - 14863 + - 18847 + - 5022 + - 14862 + - 14859 + - 14142 + - 14861 + - 21902 + - 22366 + - 17692 + - 20932 + - 13791 + - 4208 + - 9553 + - 8969 + - 17568 + - 4129 + - 21735 + - 2463 + - 5418 + - 5796 + - 766 + - 17924 + - 5712 + - 21769 + - 8098 + - 539 + - 5565 diff --git a/.context/compound-engineering/ce-optimize/vectorlite-clustering/experiment-log.yaml b/.context/compound-engineering/ce-optimize/vectorlite-clustering/experiment-log.yaml new file mode 100644 index 0000000..81246db --- /dev/null +++ b/.context/compound-engineering/ce-optimize/vectorlite-clustering/experiment-log.yaml @@ -0,0 +1,225 @@ +spec_name: vectorlite-clustering +started_at: "2026-03-29T00:00:00Z" +goal: > + Replace in-memory exact kNN with vectorlite ANN + post-processing. + Target: >=50% cluster membership (threads in multi-member clusters / total threads). + Constraint: no mega-clusters (max cluster size capped). + +baseline: + timestamp: "2026-03-29" + params: { backend: vectorlite, k: 6, min_score: 0.82, candidate_k: 96, cluster_mode: basic } + metrics: + multi_member_pct: 31.37 + edge_count: 6934 + cluster_count: 14338 + solo_clusters: 12708 + threads_in_multi: 5808 + total_threads: 18516 + max_cluster_size: 619 + avg_multi_size: 3.56 + duration_ms: 129190 + +experiments: + - id: 1 + description: "Lower threshold to 0.75 (no refinement)" + params: { k: 6, min_score: 0.75, cluster_mode: basic } + outcome: degenerate + metrics: + multi_member_pct: 68.64 + edge_count: 29157 + solo_clusters: 5807 + max_cluster_size: 9780 # mega-cluster — 53% of all threads + notes: "Union-Find transitive closure creates one giant blob" + + - id: 2 + description: "Threshold 0.78 (no refinement)" + params: { k: 6, min_score: 0.78, cluster_mode: basic } + outcome: degenerate + metrics: + multi_member_pct: 53.23 + edge_count: 16400 + solo_clusters: 8660 + max_cluster_size: 4905 + + - id: 3 + description: "Threshold 0.80 (no refinement)" + params: { k: 6, min_score: 0.80, cluster_mode: basic } + outcome: degenerate + metrics: + multi_member_pct: 42.0 + edge_count: 10744 + solo_clusters: 10739 + max_cluster_size: 2417 + + - id: 4 + description: "Threshold 0.78 with refine mode (max=100, step=0.02)" + params: { k: 6, min_score: 0.78, cluster_mode: refine, max_cluster_size: 100, refine_step: 0.02 } + outcome: improved + metrics: + multi_member_pct: 44.58 + edge_count: 16400 + solo_clusters: 10262 + max_cluster_size: 89 + avg_multi_size: 3.77 + notes: "Refinement works but creates extra solos by splitting mega-clusters with higher thresholds" + + - id: 5 + description: "t=0.75, k=10, refine (max=100, step=0.03)" + params: { k: 10, min_score: 0.75, cluster_mode: refine, max_cluster_size: 100, refine_step: 0.03 } + outcome: improved + metrics: + multi_member_pct: 47.0 + edge_count: 34913 + solo_clusters: 9814 + max_cluster_size: 94 + avg_multi_size: 3.66 + + - id: 6 + description: "t=0.75, k=10, refine (max=200, step=0.01)" + params: { k: 10, min_score: 0.75, cluster_mode: refine, max_cluster_size: 200, refine_step: 0.01 } + outcome: kept + metrics: + multi_member_pct: 52.2 + edge_count: 34913 + solo_clusters: 8850 + max_cluster_size: 164 + avg_multi_size: 4.11 + duration_ms: 156806 + cluster_build_ms: 575 + notes: "First experiment to hit 50% target" + + - id: 7 + description: "t=0.73, k=12, refine (max=200, step=0.01)" + params: { k: 12, min_score: 0.73, cluster_mode: refine, max_cluster_size: 200, refine_step: 0.01 } + outcome: improved + metrics: + multi_member_pct: 54.48 + edge_count: 54496 + solo_clusters: 8429 + max_cluster_size: 164 + avg_multi_size: 4.1 + + - id: 8 + description: "t=0.73, k=10, refine (max=200, step=0.01)" + params: { k: 10, min_score: 0.73, cluster_mode: refine, max_cluster_size: 200, refine_step: 0.01 } + outcome: improved + metrics: + multi_member_pct: 54.48 + edge_count: 50621 + solo_clusters: 8429 + max_cluster_size: 164 + avg_multi_size: 4.1 + + - id: 9 + description: "t=0.75, k=10, candidateK=200, refine (max=200, step=0.01)" + params: { k: 10, min_score: 0.75, candidate_k: 200, cluster_mode: refine, max_cluster_size: 200, refine_step: 0.01 } + outcome: no_improvement + metrics: + multi_member_pct: 52.2 + edge_count: 34913 + solo_clusters: 8850 + max_cluster_size: 164 + notes: "Higher candidateK did not help — ANN recall already sufficient" + + - id: 10 + description: "t=0.73, k=15, efSearch=300, refine (max=200, step=0.01)" + params: { k: 15, min_score: 0.73, ef_search: 300, cluster_mode: refine, max_cluster_size: 200, refine_step: 0.01 } + outcome: no_improvement + metrics: + multi_member_pct: 54.48 + edge_count: 59059 + solo_clusters: 8429 + max_cluster_size: 164 + notes: "More edges from higher k/efSearch but same membership — solos have NO neighbors above threshold" + + # --- Size-bounded Union-Find experiments (new algorithm) --- + + - id: 13 + description: "BOUNDED t=0.75, k=10, maxCluster=200" + params: { k: 10, min_score: 0.75, cluster_mode: bounded, max_cluster_size: 200 } + outcome: kept + metrics: + multi_member_pct: 64.47 + edge_count: 34913 + solo_clusters: 6579 + threads_in_multi: 11937 + max_cluster_size: 200 + avg_multi_size: 8.02 + cluster_build_ms: 30 + notes: "Dramatically better than refine — processes edges strongest-first, caps cluster size" + + - id: 14 + description: "BOUNDED t=0.75, k=10, maxCluster=100" + params: { k: 10, min_score: 0.75, cluster_mode: bounded, max_cluster_size: 100 } + outcome: kept + metrics: + multi_member_pct: 64.83 + edge_count: 34913 + solo_clusters: 6513 + threads_in_multi: 12003 + max_cluster_size: 100 + avg_multi_size: 7.89 + + - id: 15 + description: "BOUNDED t=0.78, k=10, maxCluster=200" + params: { k: 10, min_score: 0.78, cluster_mode: bounded, max_cluster_size: 200 } + outcome: improved + metrics: + multi_member_pct: 51.69 + edge_count: 18448 + solo_clusters: 8945 + threads_in_multi: 9571 + max_cluster_size: 200 + avg_multi_size: 5.46 + + - id: 16 + description: "BOUNDED t=0.73, k=10, maxCluster=200" + params: { k: 10, min_score: 0.73, cluster_mode: bounded, max_cluster_size: 200 } + outcome: best + metrics: + multi_member_pct: 72.08 + edge_count: 50621 + solo_clusters: 5169 + threads_in_multi: 13347 + max_cluster_size: 200 + avg_multi_size: 10.53 + notes: "Best overall — 72% membership with controlled cluster sizes" + + - id: 17 + description: "BOUNDED t=0.75, k=6, maxCluster=200" + params: { k: 6, min_score: 0.75, cluster_mode: bounded, max_cluster_size: 200 } + outcome: kept + metrics: + multi_member_pct: 64.46 + edge_count: 29157 + solo_clusters: 6580 + threads_in_multi: 11936 + max_cluster_size: 200 + avg_multi_size: 8.05 + notes: "k=6 vs k=10 nearly identical — solo count is threshold-driven, not k-driven" + +key_learnings: + - "Union-Find transitive closure is the root cause of mega-clusters at lower thresholds" + - "Refine mode (raise threshold to split) creates extra solos — destructive approach" + - "Size-bounded Union-Find (greedy strongest-first with size cap) is far superior" + - "k and candidateK have minimal impact once threshold is set — solos have NO neighbors above threshold" + - "efSearch has no impact — ANN recall is already good at default candidateK" + - "The threshold is the primary lever for cluster membership percentage" + - "Bounded mode preserves 64-72% membership vs refine mode's 52-54% at same threshold" + +code_changes: + - file: packages/api-core/src/cluster/build.ts + changes: + - "Added buildSizeBoundedClusters() — greedy strongest-first Union-Find with size cap" + - "Added buildRefinedClusters() — iterative threshold-raising refinement (inferior)" + - "Made UnionFind.find() iterative to avoid stack overflow on deep chains" + - "Added unionBounded() with size tracking" + - "Refactored formatClusters() as shared helper" + - file: packages/api-core/src/service.ts + changes: + - "Added clusterMode, maxClusterSize, refineStep params to clusterExperiment()" + - "Imported buildRefinedClusters and buildSizeBoundedClusters" + - file: scripts/cluster-optimize-measure.mjs + changes: + - "New measurement harness script for optimization experiments" + - "Supports all clustering params including cluster-mode" diff --git a/packages/api-core/src/openai/provider.ts b/packages/api-core/src/openai/provider.ts index cfd3b94..b83f02a 100644 --- a/packages/api-core/src/openai/provider.ts +++ b/packages/api-core/src/openai/provider.ts @@ -56,8 +56,20 @@ export class OpenAiProvider implements AiProvider { content: [ { type: 'input_text', - text: - 'Summarize this GitHub issue or pull request thread. Return concise JSON only with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field should be plain text, no markdown, and usually 1-3 sentences.', + text: [ + 'Summarize this GitHub issue or pull request for automated duplicate detection. Your summary will be embedded and clustered.', + '', + 'Structure your analysis:', + '1. First identify the COMPONENT or SUBSYSTEM (e.g., "Discord gateway", "WhatsApp delivery", "Telegram media handler", "CLI routing", "session management")', + '2. Then identify the SPECIFIC PROBLEM or CHANGE within that component', + '3. Combine into a clear dedupe_summary that starts with the component name', + '', + 'Ignore completely: template boilerplate, testing instructions, checklists, environment info, reproduction steps, deployment notes, version numbers, cross-references.', + '', + 'Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.', + 'Plain text, no markdown, 1-3 sentences each.', + 'dedupe_summary format: "[Component]: [specific issue or change]" — this helps cluster by subsystem.', + ].join('\n'), }, ], }, diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index 4515db5..fe9e7c1 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -48,7 +48,7 @@ import { type ThreadsResponse, } from '@ghcrawl/api-contract'; -import { buildClusters } from './cluster/build.js'; +import { buildClusters, buildRefinedClusters, buildSizeBoundedClusters } from './cluster/build.js'; import { buildSourceKindEdges } from './cluster/exact-edges.js'; import { ensureRuntimeDirs, @@ -996,24 +996,76 @@ export class GHCrawlService { let inputTokens = 0; let outputTokens = 0; let totalTokens = 0; - for (const [index, row] of pending.entries()) { - params.onProgress?.(`[summarize] ${index + 1}/${pending.length} thread #${row.number}`); - const result = await ai.summarizeThread({ - model: this.config.summaryModel, - text: row.summaryInput, - }); - const summary = result.summary; - - this.upsertSummary(row.id, row.summaryContentHash, 'problem_summary', summary.problemSummary); - this.upsertSummary(row.id, row.summaryContentHash, 'solution_summary', summary.solutionSummary); - this.upsertSummary(row.id, row.summaryContentHash, 'maintainer_signal_summary', summary.maintainerSignalSummary); - this.upsertSummary(row.id, row.summaryContentHash, 'dedupe_summary', summary.dedupeSummary); - if (result.usage) { - inputTokens += result.usage.inputTokens; - outputTokens += result.usage.outputTokens; - totalTokens += result.usage.totalTokens; + let cachedInputTokens = 0; + const startTime = Date.now(); + + // gpt-5.4-mini pricing per million tokens + const INPUT_COST_PER_M = 0.75; + const CACHED_INPUT_COST_PER_M = 0.075; + const OUTPUT_COST_PER_M = 4.50; + + // Stage 1: concurrent API calls + const fetcher = new IterableMapper( + pending, + async (row) => { + const result = await ai.summarizeThread({ + model: this.config.summaryModel, + text: row.summaryInput, + }); + return { row, result }; + }, + { concurrency: 5 }, + ); + + // Stage 2: sequential DB writes — consumes from fetcher without blocking API completions + const writer = new IterableMapper( + fetcher, + async ({ row, result }) => { + const summary = result.summary; + this.upsertSummary(row.id, row.summaryContentHash, 'problem_summary', summary.problemSummary); + this.upsertSummary(row.id, row.summaryContentHash, 'solution_summary', summary.solutionSummary); + this.upsertSummary(row.id, row.summaryContentHash, 'maintainer_signal_summary', summary.maintainerSignalSummary); + this.upsertSummary(row.id, row.summaryContentHash, 'dedupe_summary', summary.dedupeSummary); + return { row, usage: result.usage }; + }, + { concurrency: 1 }, + ); + + let index = 0; + for await (const { row, usage } of writer) { + index += 1; + if (usage) { + inputTokens += usage.inputTokens; + outputTokens += usage.outputTokens; + totalTokens += usage.totalTokens; + cachedInputTokens += usage.cachedInputTokens; + } + + // Compute cost and ETA every 10 items or on the last item + if (index % 10 === 0 || index === pending.length) { + const uncachedInput = inputTokens - cachedInputTokens; + const costSoFar = + (uncachedInput / 1_000_000) * INPUT_COST_PER_M + + (cachedInputTokens / 1_000_000) * CACHED_INPUT_COST_PER_M + + (outputTokens / 1_000_000) * OUTPUT_COST_PER_M; + const remaining = pending.length - index; + const avgIn = inputTokens / index; + const avgOut = outputTokens / index; + const avgCachedIn = cachedInputTokens / index; + const estTotalCost = + costSoFar + + ((remaining * (avgIn - avgCachedIn)) / 1_000_000) * INPUT_COST_PER_M + + ((remaining * avgCachedIn) / 1_000_000) * CACHED_INPUT_COST_PER_M + + ((remaining * avgOut) / 1_000_000) * OUTPUT_COST_PER_M; + + const elapsedSec = (Date.now() - startTime) / 1000; + const secPerItem = elapsedSec / index; + const etaSec = remaining * secPerItem; + const etaMin = Math.round(etaSec / 60); + const etaStr = etaMin >= 60 ? `${Math.floor(etaMin / 60)}h${etaMin % 60}m` : `${etaMin}m`; + params.onProgress?.( - `[summarize] tokens thread #${row.number} in=${result.usage.inputTokens} out=${result.usage.outputTokens} total=${result.usage.totalTokens} cached_in=${result.usage.cachedInputTokens} reasoning=${result.usage.reasoningTokens}`, + `[summarize] ${index}/${pending.length} thread #${row.number} | cost=$${costSoFar.toFixed(2)} est_total=$${estTotalCost.toFixed(2)} | avg_in=${Math.round(avgIn)} avg_out=${Math.round(avgOut)} | ETA ${etaStr}`, ); } summarized += 1; @@ -1181,6 +1233,9 @@ export class GHCrawlService { k?: number; candidateK?: number; efSearch?: number; + maxClusterSize?: number; + refineStep?: number; + clusterMode?: 'basic' | 'refine' | 'bounded'; includeClusters?: boolean; onProgress?: (message: string) => void; }): ClusterExperimentResult { @@ -1352,14 +1407,23 @@ export class GHCrawlService { } const clusterStartedAt = Date.now(); - const clusters = buildClusters( - items.map((item) => ({ threadId: item.id, number: item.number, title: item.title })), - Array.from(aggregated.values()).map((entry) => ({ - leftThreadId: entry.leftThreadId, - rightThreadId: entry.rightThreadId, - score: entry.score, - })), - ); + const clusterNodes = items.map((item) => ({ threadId: item.id, number: item.number, title: item.title })); + const clusterEdges = Array.from(aggregated.values()).map((entry) => ({ + leftThreadId: entry.leftThreadId, + rightThreadId: entry.rightThreadId, + score: entry.score, + })); + const clusterMode = params.clusterMode ?? (params.maxClusterSize !== undefined ? 'refine' : 'basic'); + const clusters = clusterMode === 'bounded' + ? buildSizeBoundedClusters(clusterNodes, clusterEdges, { + maxClusterSize: params.maxClusterSize ?? 200, + }) + : clusterMode === 'refine' + ? buildRefinedClusters(clusterNodes, clusterEdges, { + maxClusterSize: params.maxClusterSize ?? 200, + refineStep: params.refineStep ?? 0.02, + }) + : buildClusters(clusterNodes, clusterEdges); clusterBuildMs += Date.now() - clusterStartedAt; recordMemory(); const memoryAfter = process.memoryUsage(); diff --git a/scripts/cluster-optimize-measure.mjs b/scripts/cluster-optimize-measure.mjs new file mode 100644 index 0000000..d7c43a2 --- /dev/null +++ b/scripts/cluster-optimize-measure.mjs @@ -0,0 +1,136 @@ +/** + * Measurement harness for cluster optimization experiments. + * + * Runs clusterExperiment with configurable parameters and outputs JSON metrics. + * Does NOT modify the shared DB — clusterExperiment is read-only on the main DB. + * + * Usage: + * node scripts/cluster-optimize-measure.mjs [owner/repo] \ + * --k 6 --threshold 0.82 --candidate-k 96 --ef-search 200 --backend vectorlite + * + * Output: JSON object with all metrics to stdout (progress to stderr). + */ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const serviceModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'service.js'); + +const { GHCrawlService } = await import(serviceModulePath); + +function parseArgs(argv) { + let repo = 'openclaw/openclaw'; + let k; + let threshold; + let candidateK; + let efSearch; + let backend = 'vectorlite'; + let maxClusterSize; + let refineStep; + let clusterMode; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token) continue; + if (token === '--k') { k = Number(argv[++index]); continue; } + if (token === '--threshold') { threshold = Number(argv[++index]); continue; } + if (token === '--candidate-k') { candidateK = Number(argv[++index]); continue; } + if (token === '--ef-search') { efSearch = Number(argv[++index]); continue; } + if (token === '--backend') { backend = argv[++index]; continue; } + if (token === '--max-cluster-size') { maxClusterSize = Number(argv[++index]); continue; } + if (token === '--refine-step') { refineStep = Number(argv[++index]); continue; } + if (token === '--cluster-mode') { clusterMode = argv[++index]; continue; } + if (!token.startsWith('--')) repo = token; + } + + const [owner, name] = repo.split('/'); + if (!owner || !name) throw new Error(`Expected owner/repo, received: ${repo}`); + + return { + owner, + repo: name, + fullName: `${owner}/${name}`, + k: Number.isFinite(k) ? k : undefined, + threshold: Number.isFinite(threshold) ? threshold : undefined, + candidateK: Number.isFinite(candidateK) ? candidateK : undefined, + efSearch: Number.isFinite(efSearch) ? efSearch : undefined, + backend, + maxClusterSize: Number.isFinite(maxClusterSize) ? maxClusterSize : undefined, + refineStep: Number.isFinite(refineStep) ? refineStep : undefined, + clusterMode: clusterMode || undefined, + }; +} + +const args = parseArgs(process.argv.slice(2)); + +const service = new GHCrawlService(); +try { + const result = service.clusterExperiment({ + owner: args.owner, + repo: args.repo, + backend: args.backend, + k: args.k, + minScore: args.threshold, + candidateK: args.candidateK, + efSearch: args.efSearch, + maxClusterSize: args.maxClusterSize, + refineStep: args.refineStep, + clusterMode: args.clusterMode, + onProgress: (message) => process.stderr.write(`${message}\n`), + }); + + const totalThreads = result.threads; + const soloClusters = result.clusterSizes.soloClusters; + const multiMemberClusters = result.clusters - soloClusters; + const threadsInMulti = totalThreads - soloClusters; + const multiMemberPct = totalThreads > 0 ? threadsInMulti / totalThreads : 0; + + const metrics = { + // Primary metric + multi_member_pct: Math.round(multiMemberPct * 10000) / 100, + + // Gate metrics + edge_count: result.edges, + cluster_count: result.clusters, + solo_clusters: soloClusters, + multi_member_clusters: multiMemberClusters, + threads_in_multi: threadsInMulti, + total_threads: totalThreads, + max_cluster_size: result.clusterSizes.maxClusterSize, + + // Diagnostics + solo_pct: Math.round((soloClusters / Math.max(result.clusters, 1)) * 10000) / 100, + avg_multi_size: multiMemberClusters > 0 + ? Math.round((threadsInMulti / multiMemberClusters) * 100) / 100 + : 0, + + // Timing + duration_ms: result.durationMs, + total_duration_ms: result.totalDurationMs, + load_ms: result.loadMs, + setup_ms: result.setupMs, + index_build_ms: result.indexBuildMs, + query_ms: result.queryMs, + cluster_build_ms: result.clusterBuildMs, + + // Params used + params: { + backend: result.backend, + k: args.k ?? 6, + min_score: args.threshold ?? 0.82, + candidate_k: result.candidateK, + ef_search: args.efSearch ?? null, + max_cluster_size: args.maxClusterSize ?? null, + refine_step: args.refineStep ?? null, + cluster_mode: args.clusterMode ?? null, + }, + + // Size distribution (top 20) + top_cluster_sizes: result.clusterSizes.topClusterSizes.slice(0, 20), + histogram: result.clusterSizes.histogram, + }; + + process.stdout.write(JSON.stringify(metrics, null, 2) + '\n'); +} finally { + service.close(); +} diff --git a/scripts/op-run.mjs b/scripts/op-run.mjs index d5ae137..6430200 100644 --- a/scripts/op-run.mjs +++ b/scripts/op-run.mjs @@ -115,6 +115,15 @@ function main(argv = process.argv.slice(2)) { return; } + if (mode === 'run') { + const args = rest[0] === '--' ? rest.slice(1) : rest; + if (args.length === 0) { + throw new Error('Missing command. Example: node scripts/op-run.mjs run -- node scripts/my-script.mjs'); + } + runWithEnv(args[0], args.slice(1)); + return; + } + throw new Error(`Unknown mode: ${mode}`); } diff --git a/scripts/run-all-prompt-experiments.mjs b/scripts/run-all-prompt-experiments.mjs new file mode 100644 index 0000000..a532f5a --- /dev/null +++ b/scripts/run-all-prompt-experiments.mjs @@ -0,0 +1,69 @@ +#!/usr/bin/env node +/** + * Run all prompt experiments sequentially. + * Usage: node scripts/op-run.mjs run -- node scripts/run-all-prompt-experiments.mjs + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const promptDir = path.join(repoRoot, '.context', 'compound-engineering', 'ce-optimize', 'summary-prompt', 'prompts'); +const resultsDir = path.join(repoRoot, '.context', 'compound-engineering', 'ce-optimize', 'summary-prompt', 'results'); + +const promptFiles = fs.readdirSync(promptDir) + .filter(f => f.endsWith('.txt')) + .sort(); + +// Check which experiments already have results +const existing = new Set( + fs.existsSync(resultsDir) + ? fs.readdirSync(resultsDir).filter(f => f.endsWith('.json')).map(f => f.replace('.json', '')) + : [] +); + +const summaryTable = []; + +for (const file of promptFiles) { + const experimentId = file.replace('.txt', ''); + + if (existing.has(experimentId)) { + // Load existing result + const result = JSON.parse(fs.readFileSync(path.join(resultsDir, `${experimentId}.json`), 'utf8')); + const scored = result.results.filter(r => r.judge?.score != null); + if (scored.length >= 30) { + process.stderr.write(`[SKIP] ${experimentId} — already has ${scored.length} scored results\n`); + summaryTable.push({ experiment_id: experimentId, ...result.aggregate, status: 'cached' }); + continue; + } + process.stderr.write(`[RERUN] ${experimentId} — only ${scored.length} scored results, rerunning\n`); + } + + process.stderr.write(`\n=== Running ${experimentId} ===\n`); + const promptPath = path.join(promptDir, file); + + try { + const stdout = execFileSync('node', [ + path.join(repoRoot, 'scripts', 'summarize-prompt-experiment.mjs'), + 'openclaw/openclaw', + '--prompt-file', promptPath, + '--experiment-id', experimentId, + ], { + cwd: repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + timeout: 1_800_000, + env: process.env, + }); + + const result = JSON.parse(stdout.trim()); + summaryTable.push({ ...result, status: 'completed' }); + } catch (error) { + process.stderr.write(`[ERROR] ${experimentId}: ${error.message}\n`); + summaryTable.push({ experiment_id: experimentId, status: 'error', error: error.message }); + } +} + +process.stderr.write('\n\n=== SUMMARY TABLE ===\n'); +process.stdout.write(JSON.stringify(summaryTable, null, 2) + '\n'); diff --git a/scripts/summarize-prompt-experiment.mjs b/scripts/summarize-prompt-experiment.mjs new file mode 100644 index 0000000..fa27ec5 --- /dev/null +++ b/scripts/summarize-prompt-experiment.mjs @@ -0,0 +1,275 @@ +/** + * Run a summarization prompt experiment against the 40 test threads. + * Summarizes each thread, then judges the summary quality. + * + * Usage: + * node scripts/summarize-prompt-experiment.mjs \ + * --prompt-file prompts/v1.txt \ + * --experiment-id baseline \ + * --output-dir .context/compound-engineering/ce-optimize/summary-prompt/results + * + * Outputs: JSON file per experiment with all summaries and judge scores. + * Requires OPENAI_API_KEY in environment. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const serviceModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'service.js'); + +const { GHCrawlService } = await import(serviceModulePath); + +const TEST_THREAD_IDS = [ + // Issues + 15126, 8920, 19616, 16324, 10106, 14855, 18179, 2538, 9401, 9156, + 18848, 14856, 14863, 18847, 5022, 14862, 14859, 14142, 14861, 21902, + // PRs + 22366, 17692, 20932, 13791, 4208, 9553, 8969, 17568, 4129, 21735, + 2463, 5418, 5796, 766, 17924, 5712, 21769, 8098, 539, 5565, +]; + +const DEFAULT_PROMPT = 'Summarize this GitHub issue or pull request thread. Return concise JSON only with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field should be plain text, no markdown, and usually 1-3 sentences.'; + +const JUDGE_PROMPT = `You are evaluating the quality of a dedupe_summary generated from a GitHub issue or pull request. The dedupe_summary will be embedded and used for clustering similar issues together. + +A good dedupe_summary: +- Captures the CORE problem or change in 1-3 sentences +- Strips away template boilerplate, checklists, testing instructions, deployment notes +- Focuses on WHAT the issue/PR is about, not HOW it was found or tested +- Uses specific technical terms that would match similar issues (e.g., "Discord REST API proxy" not "network issue") +- Avoids generic phrases that could match unrelated issues +- Does NOT include version numbers, dates, or reproduction steps (these don't help deduplication) + +Rate the dedupe_summary on a 1-5 scale: +- 5: Perfectly captures the core issue/change. Would cluster correctly with similar items. No noise. +- 4: Good signal, minor noise or missing detail that wouldn't hurt clustering much. +- 3: Adequate but includes some noise (testing details, template artifacts) or misses a key aspect. +- 2: Weak — too generic, too verbose, or includes significant noise that could cause false matches. +- 1: Poor — misses the point, is mostly boilerplate, or would cluster incorrectly. + +Also report: +- has_boilerplate: boolean — does the summary contain template artifacts, checklists, or testing notes? +- signal_density: 1-5 — how much of the summary is useful signal vs noise? +- would_cluster_correctly: boolean — given the title, would this summary help find duplicates? + +Return JSON only with keys: score, has_boilerplate, signal_density, would_cluster_correctly, reasoning (1 sentence).`; + +function parseArgs(argv) { + let repo = 'openclaw/openclaw'; + let promptFile = null; + let promptText = null; + let experimentId = 'unnamed'; + let outputDir = '.context/compound-engineering/ce-optimize/summary-prompt/results'; + let model = null; + let judgeModel = null; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token) continue; + if (token === '--prompt-file') { promptFile = argv[++index]; continue; } + if (token === '--prompt') { promptText = argv[++index]; continue; } + if (token === '--experiment-id') { experimentId = argv[++index]; continue; } + if (token === '--output-dir') { outputDir = argv[++index]; continue; } + if (token === '--model') { model = argv[++index]; continue; } + if (token === '--judge-model') { judgeModel = argv[++index]; continue; } + if (!token.startsWith('--')) repo = token; + } + + const [owner, name] = repo.split('/'); + if (!owner || !name) throw new Error(`Expected owner/repo, received: ${repo}`); + + let systemPrompt = DEFAULT_PROMPT; + if (promptFile) { + systemPrompt = fs.readFileSync(path.resolve(promptFile), 'utf8').trim(); + } else if (promptText) { + systemPrompt = promptText; + } + + return { owner, repo: name, systemPrompt, experimentId, outputDir, model, judgeModel }; +} + +async function summarizeThread(client, model, systemPrompt, summaryInput, format, summarySchema) { + for (const [attemptIndex, maxOutputTokens] of [500, 900, 1400].entries()) { + try { + const response = await client.responses.create({ + model, + input: [ + { role: 'system', content: [{ type: 'input_text', text: systemPrompt }] }, + { role: 'user', content: [{ type: 'input_text', text: summaryInput }] }, + ], + text: { format, verbosity: 'low' }, + max_output_tokens: maxOutputTokens, + }); + const parsed = summarySchema.parse(JSON.parse(response.output_text ?? '')); + return { + summary: parsed, + usage: response.usage ? { + input_tokens: response.usage.input_tokens, + output_tokens: response.usage.output_tokens, + } : null, + }; + } catch (error) { + if (attemptIndex === 2) throw error; + } + } +} + +async function judgeResult(client, judgeModel, title, body, dedupeSummary, judgeSchema) { + const judgeInput = `Title: ${title}\n\nOriginal body (first 1000 chars): ${(body ?? '').slice(0, 1000)}\n\ndedupe_summary to evaluate: ${dedupeSummary}`; + + const response = await client.responses.create({ + model: judgeModel, + input: [ + { role: 'system', content: [{ type: 'input_text', text: JUDGE_PROMPT }] }, + { role: 'user', content: [{ type: 'input_text', text: judgeInput }] }, + ], + text: { + format: { type: 'json_schema', name: 'judge_result', strict: true, schema: { + type: 'object', + properties: { + score: { type: 'integer' }, + has_boilerplate: { type: 'boolean' }, + signal_density: { type: 'integer' }, + would_cluster_correctly: { type: 'boolean' }, + reasoning: { type: 'string' }, + }, + required: ['score', 'has_boilerplate', 'signal_density', 'would_cluster_correctly', 'reasoning'], + additionalProperties: false, + }}, + }, + max_output_tokens: 800, + }); + + const text = response.output_text ?? '{}'; + try { + return JSON.parse(text); + } catch { + process.stderr.write(` judge parse error, raw: ${text.slice(0, 200)}\n`); + return { score: null, has_boilerplate: null, signal_density: null, would_cluster_correctly: null, reasoning: `parse error: ${text.slice(0, 100)}` }; + } +} + +const args = parseArgs(process.argv.slice(2)); + +import { createRequire } from 'node:module'; +const apiCoreRequire = createRequire(path.join(repoRoot, 'packages', 'api-core', 'package.json')); +const { default: OpenAI } = await import(apiCoreRequire.resolve('openai')); +const { zodTextFormat } = await import(apiCoreRequire.resolve('openai/helpers/zod')); +const { z } = await import(apiCoreRequire.resolve('zod')); + +const apiKey = process.env.OPENAI_API_KEY; +if (!apiKey) throw new Error('OPENAI_API_KEY not set'); + +const client = new OpenAI({ apiKey }); +const service = new GHCrawlService(); +const summarySchema = z.object({ + problem_summary: z.string(), + solution_summary: z.string(), + maintainer_signal_summary: z.string(), + dedupe_summary: z.string(), +}); +const format = zodTextFormat(summarySchema, 'ghcrawl_thread_summary'); + +try { + const repository = service.requireRepository(args.owner, args.repo); + const model = args.model ?? service.config.summaryModel; + const judgeModel = args.judgeModel ?? 'gpt-5-mini'; + + const results = []; + let totalInputTokens = 0; + let totalOutputTokens = 0; + + for (const [index, threadId] of TEST_THREAD_IDS.entries()) { + const thread = service.db.prepare( + 'SELECT id, number, kind, title, body, labels_json FROM threads WHERE id = ?' + ).get(threadId); + + if (!thread) { + process.stderr.write(`[${index + 1}/${TEST_THREAD_IDS.length}] thread ${threadId} not found, skipping\n`); + continue; + } + + const body = (thread.body ?? '').replace(/\r/g, '\n').replace(/\s+/g, ' ').trim(); + const title = thread.title.replace(/\r/g, '\n').replace(/\s+/g, ' ').trim(); + const labels = JSON.parse(thread.labels_json || '[]'); + const parts = [`title: ${title}`]; + if (body) parts.push(`body: ${body}`); + if (labels.length > 0) parts.push(`labels: ${labels.join(', ')}`); + const summaryInput = parts.join('\n\n'); + + process.stderr.write(`[${index + 1}/${TEST_THREAD_IDS.length}] #${thread.number} (${thread.kind}) summarizing...\n`); + + try { + const summaryResult = await summarizeThread(client, model, args.systemPrompt, summaryInput, format, summarySchema); + if (summaryResult.usage) { + totalInputTokens += summaryResult.usage.input_tokens; + totalOutputTokens += summaryResult.usage.output_tokens; + } + + process.stderr.write(`[${index + 1}/${TEST_THREAD_IDS.length}] #${thread.number} judging...\n`); + const judgeResult_ = await judgeResult(client, judgeModel, title, thread.body, summaryResult.summary.dedupe_summary); + + results.push({ + thread_id: threadId, + number: thread.number, + kind: thread.kind, + title: thread.title, + summary: summaryResult.summary, + judge: judgeResult_, + usage: summaryResult.usage, + }); + } catch (error) { + process.stderr.write(`[${index + 1}/${TEST_THREAD_IDS.length}] #${thread.number} ERROR: ${error.message}\n`); + results.push({ + thread_id: threadId, + number: thread.number, + kind: thread.kind, + title: thread.title, + error: error.message, + }); + } + } + + // Aggregate scores + const scored = results.filter(r => r.judge?.score != null); + const avgScore = scored.length > 0 ? scored.reduce((s, r) => s + r.judge.score, 0) / scored.length : 0; + const avgSignalDensity = scored.length > 0 ? scored.reduce((s, r) => s + r.judge.signal_density, 0) / scored.length : 0; + const boilerplateCount = scored.filter(r => r.judge.has_boilerplate).length; + const wouldClusterCount = scored.filter(r => r.judge.would_cluster_correctly).length; + + const experiment = { + experiment_id: args.experimentId, + model, + judge_model: judgeModel, + system_prompt: args.systemPrompt, + timestamp: new Date().toISOString(), + aggregate: { + avg_score: Math.round(avgScore * 100) / 100, + avg_signal_density: Math.round(avgSignalDensity * 100) / 100, + boilerplate_count: boilerplateCount, + boilerplate_pct: Math.round((boilerplateCount / Math.max(scored.length, 1)) * 100), + would_cluster_correctly_pct: Math.round((wouldClusterCount / Math.max(scored.length, 1)) * 100), + total_scored: scored.length, + total_errors: results.length - scored.length, + total_input_tokens: totalInputTokens, + total_output_tokens: totalOutputTokens, + }, + results, + }; + + // Save to disk + fs.mkdirSync(path.resolve(args.outputDir), { recursive: true }); + const outputPath = path.resolve(args.outputDir, `${args.experimentId}.json`); + fs.writeFileSync(outputPath, JSON.stringify(experiment, null, 2)); + process.stderr.write(`\nResults saved to ${outputPath}\n`); + + // Print summary to stdout + process.stdout.write(JSON.stringify({ + experiment_id: args.experimentId, + ...experiment.aggregate, + prompt_preview: args.systemPrompt.slice(0, 120), + }, null, 2) + '\n'); +} finally { + service.close(); +} diff --git a/scripts/summarize-single.mjs b/scripts/summarize-single.mjs new file mode 100644 index 0000000..4f94bbc --- /dev/null +++ b/scripts/summarize-single.mjs @@ -0,0 +1,145 @@ +/** + * Summarize a single thread with an optional system prompt override. + * Outputs the summary JSON to stdout. Does NOT save to DB. + * + * Usage: + * node scripts/summarize-single.mjs [--prompt-file ] + * node scripts/summarize-single.mjs [--prompt ""] + * + * Requires OPENAI_API_KEY in environment (use pnpm op:shell or op:exec). + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const serviceModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'service.js'); +const providerModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'openai', 'provider.js'); + +const { GHCrawlService } = await import(serviceModulePath); +const { OpenAiProvider } = await import(providerModulePath); + +function parseArgs(argv) { + let repo = null; + let threadNumber = null; + let promptFile = null; + let promptText = null; + let model = null; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token) continue; + if (token === '--prompt-file') { promptFile = argv[++index]; continue; } + if (token === '--prompt') { promptText = argv[++index]; continue; } + if (token === '--model') { model = argv[++index]; continue; } + if (!token.startsWith('--')) { + if (!repo) { repo = token; continue; } + if (!threadNumber) { threadNumber = Number(token); continue; } + } + } + + if (!repo || !threadNumber) { + throw new Error('Usage: summarize-single.mjs [--prompt-file ] [--prompt ""] [--model ]'); + } + + const [owner, name] = repo.split('/'); + if (!owner || !name) throw new Error(`Expected owner/repo, received: ${repo}`); + + let systemPrompt = null; + if (promptFile) { + systemPrompt = fs.readFileSync(promptFile, 'utf8').trim(); + } else if (promptText) { + systemPrompt = promptText; + } + + return { owner, repo: name, threadNumber, systemPrompt, model }; +} + +const args = parseArgs(process.argv.slice(2)); + +const service = new GHCrawlService(); +try { + const repository = service.requireRepository(args.owner, args.repo); + + // Load thread data + const thread = service.db.prepare( + 'SELECT id, number, title, body, labels_json FROM threads WHERE repo_id = ? AND number = ?' + ).get(repository.id, args.threadNumber); + + if (!thread) { + throw new Error(`Thread #${args.threadNumber} not found in ${args.owner}/${args.repo}`); + } + + // Build summary input (same as service.buildSummarySource but accessible here) + const body = (thread.body ?? '').replace(/\r/g, '\n').replace(/\s+/g, ' ').trim(); + const title = thread.title.replace(/\r/g, '\n').replace(/\s+/g, ' ').trim(); + const labels = JSON.parse(thread.labels_json || '[]'); + + const parts = [`title: ${title}`]; + if (body) parts.push(`body: ${body}`); + if (labels.length > 0) parts.push(`labels: ${labels.join(', ')}`); + const summaryInput = parts.join('\n\n'); + + // Default system prompt (matches current production prompt) + const defaultPrompt = 'Summarize this GitHub issue or pull request thread. Return concise JSON only with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field should be plain text, no markdown, and usually 1-3 sentences.'; + + const systemPrompt = args.systemPrompt ?? defaultPrompt; + const model = args.model ?? service.config.summaryModel; + + // Call OpenAI directly with optional prompt override + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + throw new Error('OPENAI_API_KEY not set. Use pnpm op:shell or set the env var.'); + } + + const { default: OpenAI } = await import('openai'); + const { zodTextFormat } = await import('openai/helpers/zod'); + const { z } = await import('zod'); + + const summarySchema = z.object({ + problem_summary: z.string(), + solution_summary: z.string(), + maintainer_signal_summary: z.string(), + dedupe_summary: z.string(), + }); + + const client = new OpenAI({ apiKey }); + const format = zodTextFormat(summarySchema, 'ghcrawl_thread_summary'); + + const response = await client.responses.create({ + model, + input: [ + { + role: 'system', + content: [{ type: 'input_text', text: systemPrompt }], + }, + { + role: 'user', + content: [{ type: 'input_text', text: summaryInput }], + }, + ], + text: { format, verbosity: 'low' }, + max_output_tokens: 900, + }); + + const parsed = summarySchema.parse(JSON.parse(response.output_text ?? '')); + + const result = { + thread_number: args.threadNumber, + thread_id: thread.id, + title: thread.title, + model, + system_prompt_preview: systemPrompt.slice(0, 100) + (systemPrompt.length > 100 ? '...' : ''), + input_length: summaryInput.length, + summary: parsed, + usage: response.usage ? { + input_tokens: response.usage.input_tokens, + output_tokens: response.usage.output_tokens, + total_tokens: response.usage.total_tokens, + } : null, + }; + + process.stdout.write(JSON.stringify(result, null, 2) + '\n'); +} finally { + service.close(); +} From cb58005f1266a9792ea976f636c88e5118093c0d Mon Sep 17 00:00:00 2001 From: huntharo Date: Mon, 30 Mar 2026 21:47:46 -0400 Subject: [PATCH 18/34] feat: add source-kind selection and score aggregation to cluster experiments Add --source-kinds and --aggregation params to clusterExperiment, supporting: - Source kind filtering (title, body, dedupe_summary, any combination) - Aggregation methods: max, mean, weighted, min-of-2, boost Collect per-source-kind scores then finalize with chosen method, replacing the hardcoded max() aggregation. This enables experiments comparing how different embedding signals and combination strategies affect cluster quality. Co-Authored-By: Claude Opus 4.6 --- .../embedding-clustering/spec.yaml | 123 +++++++++++ packages/api-core/src/cluster/build.ts | 205 +++++++++++++++++- packages/api-core/src/service.ts | 131 +++++++++-- scripts/cluster-optimize-measure.mjs | 15 ++ 4 files changed, 450 insertions(+), 24 deletions(-) create mode 100644 .context/compound-engineering/ce-optimize/embedding-clustering/spec.yaml diff --git a/.context/compound-engineering/ce-optimize/embedding-clustering/spec.yaml b/.context/compound-engineering/ce-optimize/embedding-clustering/spec.yaml new file mode 100644 index 0000000..722f950 --- /dev/null +++ b/.context/compound-engineering/ce-optimize/embedding-clustering/spec.yaml @@ -0,0 +1,123 @@ +name: embedding-clustering +description: > + Optimize clustering quality by exploring which embedding source kinds to use + (title, body, dedupe_summary), how to aggregate scores across source kinds + (max, mean, weighted, require-N-sources), and parameter tuning (k, minScore, + maxClusterSize). Uses LLM-as-judge to evaluate cluster coherence and + singleton false-negative rate. + +metric: + primary: + type: judge + name: cluster_coherence + direction: maximize + + degenerate_gates: + - name: solo_pct + check: "<= 95" + description: "Reject if >=95% of clusters are singletons (everything is solo)" + - name: max_cluster_size + check: "<= 500" + description: "Reject mega-clusters that merged unrelated items" + - name: multi_member_pct + check: ">= 5" + description: "At least 5% of threads must be in multi-member clusters" + + diagnostics: + - name: edge_count + - name: cluster_count + - name: solo_clusters + - name: multi_member_clusters + - name: threads_in_multi + - name: total_threads + - name: avg_multi_size + - name: duration_ms + + judge: + model: sonnet + sample_size: 30 + stratification: + - bucket: top_by_size + count: 10 + - bucket: mid_range + count: 10 + - bucket: small_clusters + count: 10 + singleton_sample: 15 + singleton_rubric: | + This thread is currently a SINGLETON — it was not grouped with any other thread. + Given the thread's title and dedupe_summary, evaluate whether this is correct. + + Rate 1-5: + - 5: Clearly unique, no plausible duplicates exist in a repo of 18k issues/PRs + - 4: Probably unique, though a loose connection to other topics is possible + - 3: Uncertain — could go either way, might have related threads + - 2: Likely should be grouped — the topic is common enough to have duplicates + - 1: Obvious false negative — this clearly belongs with other threads + + Return JSON: { score, reasoning } + rubric: | + You are evaluating a cluster of GitHub issues/PRs that were grouped together + by embedding similarity. Each item shows its number, kind (issue/PR), title, + and dedupe_summary. + + Rate this cluster 1-5 for COHERENCE: + - 5: All items clearly about the same specific issue, feature, or component + - 4: Strong theme with minor outliers (1 loosely related item) + - 3: Related topic area but covers 2-3 distinct sub-topics that could be split + - 2: Weak connection — items share superficial similarity only (e.g., same component name but different issues) + - 1: Unrelated items grouped together, no meaningful connection + + Also report: + - distinct_topics: integer — how many distinct sub-topics are in this cluster + - outlier_count: integer — items that don't belong + - dominant_theme: string — 1 sentence describing the main topic + + Return JSON: { score, distinct_topics, outlier_count, dominant_theme, reasoning } + scoring: + primary: mean_score + secondary: + - mean_distinct_topics + - outlier_rate + - singleton_score + sample_seed: 42 + batch_size: 5 + minimum_improvement: 0.3 + max_total_cost_usd: 20 + +measurement: + command: > + node scripts/cluster-optimize-measure.mjs openclaw/openclaw + --backend vectorlite --cluster-mode bounded --max-cluster-size 200 + timeout_seconds: 300 + stability: + mode: stable + +scope: + mutable: + - packages/api-core/src/service.ts + - packages/api-core/src/cluster/ + - packages/api-core/src/openai/provider.ts + - scripts/cluster-optimize-measure.mjs + immutable: + - packages/api-core/src/db/ + +execution: + mode: serial + backend: worktree + max_concurrent: 1 + +parallel: + shared_files: + - "~/.config/ghcrawl/ghcrawl.db" + +constraints: + - "clusterExperiment is read-only on the main DB — it creates a temp vectorlite DB" + - "Do not modify the embedding pipeline or database schema" + - "All experiments must use the same DB (shared_files)" + - "The measurement harness must accept source-kinds and aggregation as CLI args" + +stopping: + max_iterations: 30 + max_hours: 6 + plateau_iterations: 8 diff --git a/packages/api-core/src/cluster/build.ts b/packages/api-core/src/cluster/build.ts index e585305..b956fa1 100644 --- a/packages/api-core/src/cluster/build.ts +++ b/packages/api-core/src/cluster/build.ts @@ -12,30 +12,60 @@ type Node = { class UnionFind { private readonly parent = new Map(); + private readonly size = new Map(); add(value: number): void { - if (!this.parent.has(value)) this.parent.set(value, value); + if (!this.parent.has(value)) { + this.parent.set(value, value); + this.size.set(value, 1); + } } find(value: number): number { - const parent = this.parent.get(value); + let parent = this.parent.get(value); if (parent === undefined) { this.parent.set(value, value); + this.size.set(value, 1); return value; } - if (parent === value) return value; - const root = this.find(parent); - this.parent.set(value, root); - return root; + // Iterative path-finding to avoid stack overflow on deep chains + let current: number = value; + while (parent !== current) { + const grandparent: number = this.parent.get(parent) ?? parent; + this.parent.set(current, grandparent); // path splitting + current = parent; + parent = grandparent; + } + return current; } union(left: number, right: number): void { const leftRoot = this.find(left); const rightRoot = this.find(right); if (leftRoot !== rightRoot) { + const leftSize = this.size.get(leftRoot) ?? 1; + const rightSize = this.size.get(rightRoot) ?? 1; this.parent.set(rightRoot, leftRoot); + this.size.set(leftRoot, leftSize + rightSize); } } + + /** Merge only if the combined component would not exceed maxSize. Returns true if merged. */ + unionBounded(left: number, right: number, maxSize: number): boolean { + const leftRoot = this.find(left); + const rightRoot = this.find(right); + if (leftRoot === rightRoot) return true; // already same component + const leftSize = this.size.get(leftRoot) ?? 1; + const rightSize = this.size.get(rightRoot) ?? 1; + if (leftSize + rightSize > maxSize) return false; + this.parent.set(rightRoot, leftRoot); + this.size.set(leftRoot, leftSize + rightSize); + return true; + } + + getSize(value: number): number { + return this.size.get(this.find(value)) ?? 1; + } } export function buildClusters(nodes: Node[], edges: SimilarityEdge[]): Array<{ representativeThreadId: number; members: number[] }> { @@ -51,6 +81,169 @@ export function buildClusters(nodes: Node[], edges: SimilarityEdge[]): Array<{ r byRoot.set(root, list); } + return formatClusters(nodes, edges, byRoot); +} + +/** + * Build clusters with size-bounded Union-Find. + * + * Process edges from highest to lowest score, merging components only when + * the combined size stays within `maxClusterSize`. Strongest connections are + * preserved; weaker edges that would create oversized clusters are skipped. + * This avoids the "threshold raising" problem where splitting mega-clusters + * creates many solos. + */ +export function buildSizeBoundedClusters( + nodes: Node[], + edges: SimilarityEdge[], + options: { maxClusterSize: number }, +): Array<{ representativeThreadId: number; members: number[] }> { + const uf = new UnionFind(); + for (const node of nodes) uf.add(node.threadId); + + // Sort edges by score descending — strongest connections first + const sortedEdges = [...edges].sort((a, b) => b.score - a.score); + const keptEdges: SimilarityEdge[] = []; + + for (const edge of sortedEdges) { + if (uf.unionBounded(edge.leftThreadId, edge.rightThreadId, options.maxClusterSize)) { + keptEdges.push(edge); + } + } + + const byRoot = new Map(); + for (const node of nodes) { + const root = uf.find(node.threadId); + const list = byRoot.get(root) ?? []; + list.push(node.threadId); + byRoot.set(root, list); + } + + return formatClusters(nodes, keptEdges, byRoot); +} + +/** + * Build clusters with iterative refinement of oversized components. + * + * 1. Run Union-Find at the base threshold (edges already filtered by minScore). + * 2. For any cluster above `maxClusterSize`, re-cluster its members using only + * edges above a progressively higher threshold (raised by `refineStep` each + * iteration) until all clusters are within limits or threshold reaches 1.0. + */ +export function buildRefinedClusters( + nodes: Node[], + edges: SimilarityEdge[], + options: { maxClusterSize: number; refineStep: number }, +): Array<{ representativeThreadId: number; members: number[] }> { + const nodesById = new Map(nodes.map((node) => [node.threadId, node])); + const result: Array<{ representativeThreadId: number; members: number[] }> = []; + + // Initial Union-Find pass + const uf = new UnionFind(); + for (const node of nodes) uf.add(node.threadId); + for (const edge of edges) uf.union(edge.leftThreadId, edge.rightThreadId); + + const byRoot = new Map(); + for (const node of nodes) { + const root = uf.find(node.threadId); + const list = byRoot.get(root) ?? []; + list.push(node.threadId); + byRoot.set(root, list); + } + + // Build adjacency list for O(E) iteration instead of O(n²) pair scanning + const adjacency = new Map(); + for (const edge of edges) { + let list = adjacency.get(edge.leftThreadId); + if (!list) { list = []; adjacency.set(edge.leftThreadId, list); } + list.push(edge); + let rList = adjacency.get(edge.rightThreadId); + if (!rList) { rList = []; adjacency.set(edge.rightThreadId, rList); } + rList.push(edge); + } + + // Process each initial cluster + type WorkItem = { memberIds: number[]; currentThreshold: number }; + const workQueue: WorkItem[] = []; + + for (const members of byRoot.values()) { + if (members.length <= options.maxClusterSize) { + const clusterNodes = members.map((id) => nodesById.get(id)).filter((n): n is Node => n !== undefined); + const clusterEdges = edgesWithinSet(new Set(members), adjacency); + result.push(...formatClusters(clusterNodes, clusterEdges, new Map([[0, members]]))); + } else { + workQueue.push({ memberIds: members, currentThreshold: 0 }); + } + } + + // Iteratively refine oversized clusters + while (workQueue.length > 0) { + const item = workQueue.pop()!; + const newThreshold = item.currentThreshold + options.refineStep; + if (newThreshold >= 1.0) { + for (const memberId of item.memberIds) { + result.push({ representativeThreadId: memberId, members: [memberId] }); + } + continue; + } + + // Filter edges within this component to the higher threshold + const memberSet = new Set(item.memberIds); + const filteredEdges: SimilarityEdge[] = []; + for (const memberId of item.memberIds) { + for (const edge of adjacency.get(memberId) ?? []) { + const otherId = edge.leftThreadId === memberId ? edge.rightThreadId : edge.leftThreadId; + if (otherId > memberId && memberSet.has(otherId) && edge.score >= newThreshold) { + filteredEdges.push(edge); + } + } + } + + // Re-cluster with filtered edges + const subUf = new UnionFind(); + for (const memberId of item.memberIds) subUf.add(memberId); + for (const edge of filteredEdges) subUf.union(edge.leftThreadId, edge.rightThreadId); + + const subByRoot = new Map(); + for (const memberId of item.memberIds) { + const root = subUf.find(memberId); + const list = subByRoot.get(root) ?? []; + list.push(memberId); + subByRoot.set(root, list); + } + + for (const subMembers of subByRoot.values()) { + if (subMembers.length <= options.maxClusterSize) { + const clusterNodes = subMembers.map((id) => nodesById.get(id)).filter((n): n is Node => n !== undefined); + const clusterEdges = edgesWithinSet(new Set(subMembers), adjacency); + result.push(...formatClusters(clusterNodes, clusterEdges, new Map([[0, subMembers]]))); + } else { + workQueue.push({ memberIds: subMembers, currentThreshold: newThreshold }); + } + } + } + + return result.sort((left, right) => right.members.length - left.members.length); +} + +function edgesWithinSet(memberSet: Set, adjacency: Map): SimilarityEdge[] { + const edges: SimilarityEdge[] = []; + for (const memberId of memberSet) { + for (const edge of adjacency.get(memberId) ?? []) { + const otherId = edge.leftThreadId === memberId ? edge.rightThreadId : edge.leftThreadId; + if (otherId > memberId && memberSet.has(otherId)) { + edges.push(edge); + } + } + } + return edges; +} + +function formatClusters( + nodes: Node[], + edges: SimilarityEdge[], + byRoot: Map, +): Array<{ representativeThreadId: number; members: number[] }> { const edgeCounts = new Map(); for (const edge of edges) { edgeCounts.set(edge.leftThreadId, (edgeCounts.get(edge.leftThreadId) ?? 0) + 1); diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index fe9e7c1..98e2b94 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -1237,11 +1237,17 @@ export class GHCrawlService { refineStep?: number; clusterMode?: 'basic' | 'refine' | 'bounded'; includeClusters?: boolean; + sourceKinds?: EmbeddingSourceKind[]; + aggregation?: 'max' | 'mean' | 'weighted' | 'min-of-2' | 'boost'; + aggregationWeights?: Partial>; onProgress?: (message: string) => void; }): ClusterExperimentResult { const backend = params.backend ?? 'vectorlite'; const repository = this.requireRepository(params.owner, params.repo); - const { items, sourceKinds } = this.loadClusterableThreadMeta(repository.id); + const loaded = this.loadClusterableThreadMeta(repository.id); + const sourceKinds = params.sourceKinds ?? loaded.sourceKinds; + const items = loaded.items; + const aggregation = params.aggregation ?? 'max'; const minScore = params.minScore ?? 0.82; const k = params.k ?? 6; const candidateK = Math.max(k, params.candidateK ?? Math.max(k * 16, 64)); @@ -1258,10 +1264,10 @@ export class GHCrawlService { recordMemory(); params.onProgress?.( - `[cluster-experiment] loaded ${items.length} embedded thread(s) across ${sourceKinds.length} source kind(s) for ${repository.fullName} backend=${backend} k=${k} candidateK=${candidateK} minScore=${minScore}`, + `[cluster-experiment] loaded ${items.length} embedded thread(s) across ${sourceKinds.length} source kind(s) for ${repository.fullName} backend=${backend} k=${k} candidateK=${candidateK} minScore=${minScore} aggregation=${aggregation}`, ); - const aggregated = new Map }>(); + const perSourceScores = new Map }>(); let loadMs = 0; let setupMs = 0; let edgeBuildMs = 0; @@ -1292,13 +1298,13 @@ export class GHCrawlService { recordMemory(); if (!params.onProgress) return; params.onProgress( - `[cluster-experiment] exact ${processedItems + progress.processedItems}/${totalItems} source embeddings processed current_edges~=${aggregated.size + progress.currentEdgeEstimate}`, + `[cluster-experiment] exact ${processedItems + progress.processedItems}/${totalItems} source embeddings processed current_edges~=${perSourceScores.size + progress.currentEdgeEstimate}`, ); }, }); edgeBuildMs += Date.now() - edgesStartedAt; processedItems += normalizedRows.length; - this.mergeSourceKindEdges(aggregated, edges, sourceKind); + this.collectSourceKindScores(perSourceScores, edges, sourceKind); recordMemory(); } } else { @@ -1375,17 +1381,17 @@ export class GHCrawlService { for (const candidate of ranked) { const score = candidate.score; const key = this.edgeKey(row.id, candidate.item.rowid); - const existing = aggregated.get(key); + const existing = perSourceScores.get(key); if (existing) { - existing.score = Math.max(existing.score, score); - existing.sourceKinds.add(sourceKind); + existing.scores.set(sourceKind, Math.max(existing.scores.get(sourceKind) ?? -1, score)); continue; } - aggregated.set(key, { + const scores = new Map(); + scores.set(sourceKind, score); + perSourceScores.set(key, { leftThreadId: Math.min(row.id, candidate.item.rowid), rightThreadId: Math.max(row.id, candidate.item.rowid), - score, - sourceKinds: new Set([sourceKind]), + scores, }); addedThisRow += 1; } @@ -1394,7 +1400,7 @@ export class GHCrawlService { if (params.onProgress && now - lastProgressAt >= CLUSTER_PROGRESS_INTERVAL_MS) { recordMemory(); params.onProgress( - `[cluster-experiment] querying ${sourceKind} index ${processed}/${sourceRowCount} current_edges=${aggregated.size} added_this_step=${addedThisRow}`, + `[cluster-experiment] querying ${sourceKind} index ${processed}/${sourceRowCount} current_edges=${perSourceScores.size} added_this_step=${addedThisRow}`, ); lastProgressAt = now; } @@ -1406,13 +1412,18 @@ export class GHCrawlService { } } + // Finalize edge scores using the configured aggregation method + const defaultWeights: Record = { dedupe_summary: 0.5, title: 0.3, body: 0.2 }; + const weights = { ...defaultWeights, ...(params.aggregationWeights ?? {}) }; + const aggregated = this.finalizeEdgeScores(perSourceScores, aggregation, weights, minScore); + + params.onProgress?.( + `[cluster-experiment] finalized ${aggregated.length} edges from ${perSourceScores.size} candidate pairs using ${aggregation} aggregation`, + ); + const clusterStartedAt = Date.now(); const clusterNodes = items.map((item) => ({ threadId: item.id, number: item.number, title: item.title })); - const clusterEdges = Array.from(aggregated.values()).map((entry) => ({ - leftThreadId: entry.leftThreadId, - rightThreadId: entry.rightThreadId, - score: entry.score, - })); + const clusterEdges = aggregated; const clusterMode = params.clusterMode ?? (params.maxClusterSize !== undefined ? 'refine' : 'basic'); const clusters = clusterMode === 'bounded' ? buildSizeBoundedClusters(clusterNodes, clusterEdges, { @@ -1439,7 +1450,7 @@ export class GHCrawlService { tempDbPath, threads: items.length, sourceKinds: sourceKinds.length, - edges: aggregated.size, + edges: aggregated.length, clusters: clusters.length, timingBasis: 'cluster-only', durationMs, @@ -3586,6 +3597,90 @@ export class GHCrawlService { } } + private collectSourceKindScores( + perSourceScores: Map }>, + edges: Array<{ leftThreadId: number; rightThreadId: number; score: number }>, + sourceKind: EmbeddingSourceKind, + ): void { + for (const edge of edges) { + const key = this.edgeKey(edge.leftThreadId, edge.rightThreadId); + const existing = perSourceScores.get(key); + if (existing) { + existing.scores.set(sourceKind, Math.max(existing.scores.get(sourceKind) ?? -1, edge.score)); + continue; + } + const scores = new Map(); + scores.set(sourceKind, edge.score); + perSourceScores.set(key, { + leftThreadId: edge.leftThreadId, + rightThreadId: edge.rightThreadId, + scores, + }); + } + } + + private finalizeEdgeScores( + perSourceScores: Map }>, + aggregation: 'max' | 'mean' | 'weighted' | 'min-of-2' | 'boost', + weights: Record, + minScore: number, + ): Array<{ leftThreadId: number; rightThreadId: number; score: number }> { + const result: Array<{ leftThreadId: number; rightThreadId: number; score: number }> = []; + + for (const entry of perSourceScores.values()) { + const scoreValues = Array.from(entry.scores.values()); + let finalScore: number; + + switch (aggregation) { + case 'max': + finalScore = Math.max(...scoreValues); + break; + + case 'mean': + finalScore = scoreValues.reduce((a, b) => a + b, 0) / scoreValues.length; + break; + + case 'weighted': { + let weightedSum = 0; + let weightSum = 0; + for (const [kind, score] of entry.scores) { + const w = weights[kind] ?? 0.1; + weightedSum += score * w; + weightSum += w; + } + finalScore = weightSum > 0 ? weightedSum / weightSum : 0; + break; + } + + case 'min-of-2': + // Require at least 2 source kinds to agree (both above minScore) + if (scoreValues.length < 2) { + continue; // Skip edges with only 1 source kind + } + finalScore = Math.max(...scoreValues); + break; + + case 'boost': { + // Best score + bonus per additional agreeing source + const best = Math.max(...scoreValues); + const bonusSources = scoreValues.length - 1; + finalScore = Math.min(1.0, best + bonusSources * 0.05); + break; + } + } + + if (finalScore >= minScore) { + result.push({ + leftThreadId: entry.leftThreadId, + rightThreadId: entry.rightThreadId, + score: finalScore, + }); + } + } + + return result; + } + private countEmbeddingsForSourceKind(repoId: number, sourceKind: EmbeddingSourceKind): number { const row = this.db .prepare( diff --git a/scripts/cluster-optimize-measure.mjs b/scripts/cluster-optimize-measure.mjs index d7c43a2..1568623 100644 --- a/scripts/cluster-optimize-measure.mjs +++ b/scripts/cluster-optimize-measure.mjs @@ -28,6 +28,9 @@ function parseArgs(argv) { let maxClusterSize; let refineStep; let clusterMode; + let sourceKinds; + let aggregation; + let aggregationWeights; for (let index = 0; index < argv.length; index += 1) { const token = argv[index]; @@ -40,6 +43,9 @@ function parseArgs(argv) { if (token === '--max-cluster-size') { maxClusterSize = Number(argv[++index]); continue; } if (token === '--refine-step') { refineStep = Number(argv[++index]); continue; } if (token === '--cluster-mode') { clusterMode = argv[++index]; continue; } + if (token === '--source-kinds') { sourceKinds = argv[++index].split(','); continue; } + if (token === '--aggregation') { aggregation = argv[++index]; continue; } + if (token === '--weights') { aggregationWeights = JSON.parse(argv[++index]); continue; } if (!token.startsWith('--')) repo = token; } @@ -58,6 +64,9 @@ function parseArgs(argv) { maxClusterSize: Number.isFinite(maxClusterSize) ? maxClusterSize : undefined, refineStep: Number.isFinite(refineStep) ? refineStep : undefined, clusterMode: clusterMode || undefined, + sourceKinds: sourceKinds || undefined, + aggregation: aggregation || undefined, + aggregationWeights: aggregationWeights || undefined, }; } @@ -76,6 +85,9 @@ try { maxClusterSize: args.maxClusterSize, refineStep: args.refineStep, clusterMode: args.clusterMode, + sourceKinds: args.sourceKinds, + aggregation: args.aggregation, + aggregationWeights: args.aggregationWeights, onProgress: (message) => process.stderr.write(`${message}\n`), }); @@ -123,6 +135,9 @@ try { max_cluster_size: args.maxClusterSize ?? null, refine_step: args.refineStep ?? null, cluster_mode: args.clusterMode ?? null, + source_kinds: args.sourceKinds ?? null, + aggregation: args.aggregation ?? 'max', + aggregation_weights: args.aggregationWeights ?? null, }, // Size distribution (top 20) From 9b1f9fccc2c5b507bfd213f97cdc725104714875 Mon Sep 17 00:00:00 2001 From: huntharo Date: Mon, 30 Mar 2026 21:50:20 -0400 Subject: [PATCH 19/34] feat: add LLM-as-judge cluster experiment runner Creates cluster-judge-experiment.mjs that runs clusterExperiment with configurable params, samples clusters (stratified by size), judges them with an LLM for coherence scoring, and evaluates singletons for false negatives. Includes batch runner for 15 planned experiments. Co-Authored-By: Claude Opus 4.6 --- scripts/cluster-judge-experiment.mjs | 416 +++++++++++++++++++++++++++ scripts/run-cluster-experiments.mjs | 99 +++++++ 2 files changed, 515 insertions(+) create mode 100644 scripts/cluster-judge-experiment.mjs create mode 100644 scripts/run-cluster-experiments.mjs diff --git a/scripts/cluster-judge-experiment.mjs b/scripts/cluster-judge-experiment.mjs new file mode 100644 index 0000000..d89f33b --- /dev/null +++ b/scripts/cluster-judge-experiment.mjs @@ -0,0 +1,416 @@ +#!/usr/bin/env node +/** + * Run a clustering experiment with LLM-as-judge evaluation. + * + * 1. Runs clusterExperiment with given params + * 2. Samples clusters (stratified: top-by-size, mid-range, small) + * 3. Samples singletons for false-negative evaluation + * 4. Judges each sample with an LLM + * 5. Outputs aggregate scores + full results JSON + * + * Usage: + * node scripts/cluster-judge-experiment.mjs openclaw/openclaw \ + * --experiment-id baseline \ + * --source-kinds title,body,dedupe_summary \ + * --aggregation max \ + * --threshold 0.82 \ + * --output-dir .context/compound-engineering/ce-optimize/embedding-clustering/results + * + * Requires OPENAI_API_KEY in environment. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const serviceModulePath = path.join(repoRoot, 'packages', 'api-core', 'dist', 'service.js'); +const { GHCrawlService } = await import(serviceModulePath); + +const apiCoreRequire = createRequire(path.join(repoRoot, 'packages', 'api-core', 'package.json')); +const { default: OpenAI } = await import(apiCoreRequire.resolve('openai')); + +const CLUSTER_RUBRIC = `You are evaluating a cluster of GitHub issues/PRs that were grouped together by embedding similarity. Each item shows its number, kind (issue/PR), title, and dedupe_summary. + +Rate this cluster 1-5 for COHERENCE: +- 5: All items clearly about the same specific issue, feature, or component +- 4: Strong theme with minor outliers (1 loosely related item) +- 3: Related topic area but covers 2-3 distinct sub-topics that could be split +- 2: Weak connection — items share superficial similarity only +- 1: Unrelated items grouped together, no meaningful connection + +Also report: +- distinct_topics: integer — how many distinct sub-topics are in this cluster +- outlier_count: integer — items that don't belong +- dominant_theme: string — 1 sentence describing the main topic + +Return JSON only: { "score": , "distinct_topics": , "outlier_count": , "dominant_theme": "", "reasoning": "" }`; + +const SINGLETON_RUBRIC = `This GitHub thread is currently a SINGLETON — it was not grouped with any other thread in a repository of ~18k issues/PRs. Given its title and dedupe_summary, evaluate whether this is correct. + +Rate 1-5: +- 5: Clearly unique topic, no plausible duplicates would exist +- 4: Probably unique, though a loose connection to other topics is possible +- 3: Uncertain — could go either way, might have related threads +- 2: Likely should be grouped — the topic is common enough to have duplicates +- 1: Obvious false negative — this clearly belongs with other threads on a common topic + +Return JSON only: { "score": , "reasoning": "" }`; + +function parseArgs(argv) { + let repo = 'openclaw/openclaw'; + let experimentId = 'unnamed'; + let outputDir = '.context/compound-engineering/ce-optimize/embedding-clustering/results'; + let sourceKinds; + let aggregation; + let aggregationWeights; + let threshold; + let k; + let candidateK; + let efSearch; + let maxClusterSize = 200; + let clusterMode = 'bounded'; + let clusterSampleSize = 30; + let singletonSampleSize = 15; + let judgeModel = 'gpt-5-mini'; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (!token) continue; + if (token === '--experiment-id') { experimentId = argv[++index]; continue; } + if (token === '--output-dir') { outputDir = argv[++index]; continue; } + if (token === '--source-kinds') { sourceKinds = argv[++index].split(','); continue; } + if (token === '--aggregation') { aggregation = argv[++index]; continue; } + if (token === '--weights') { aggregationWeights = JSON.parse(argv[++index]); continue; } + if (token === '--threshold') { threshold = Number(argv[++index]); continue; } + if (token === '--k') { k = Number(argv[++index]); continue; } + if (token === '--candidate-k') { candidateK = Number(argv[++index]); continue; } + if (token === '--ef-search') { efSearch = Number(argv[++index]); continue; } + if (token === '--max-cluster-size') { maxClusterSize = Number(argv[++index]); continue; } + if (token === '--cluster-mode') { clusterMode = argv[++index]; continue; } + if (token === '--cluster-sample-size') { clusterSampleSize = Number(argv[++index]); continue; } + if (token === '--singleton-sample-size') { singletonSampleSize = Number(argv[++index]); continue; } + if (token === '--judge-model') { judgeModel = argv[++index]; continue; } + if (!token.startsWith('--')) repo = token; + } + + const [owner, name] = repo.split('/'); + return { + owner, repo: name, + experimentId, outputDir, + sourceKinds, aggregation, aggregationWeights, + threshold, k, candidateK, efSearch, + maxClusterSize, clusterMode, + clusterSampleSize, singletonSampleSize, + judgeModel, + }; +} + +function sampleClusters(clusters, sampleSize, seed = 42) { + // Separate multi-member clusters from singletons + const multiMember = clusters.filter(c => c.memberThreadIds.length > 1); + const singletons = clusters.filter(c => c.memberThreadIds.length === 1); + + // Sort by size descending + multiMember.sort((a, b) => b.memberThreadIds.length - a.memberThreadIds.length); + + const perBucket = Math.floor(sampleSize / 3); + const sampled = []; + + // Top by size + sampled.push(...multiMember.slice(0, perBucket).map(c => ({ ...c, bucket: 'top_by_size' }))); + + // Mid range + const midStart = Math.floor(multiMember.length * 0.3); + const midEnd = Math.floor(multiMember.length * 0.7); + const midPool = multiMember.slice(midStart, midEnd); + // Deterministic pseudo-random selection + const midSampled = deterministicSample(midPool, perBucket, seed); + sampled.push(...midSampled.map(c => ({ ...c, bucket: 'mid_range' }))); + + // Small clusters (size 2-3) + const smallPool = multiMember.filter(c => c.memberThreadIds.length <= 3); + const remaining = sampleSize - sampled.length; + const smallSampled = deterministicSample(smallPool, remaining, seed + 1); + sampled.push(...smallSampled.map(c => ({ ...c, bucket: 'small_clusters' }))); + + return { sampled, singletons }; +} + +function deterministicSample(pool, count, seed) { + if (pool.length <= count) return [...pool]; + // Simple seeded shuffle + const indices = pool.map((_, i) => i); + let s = seed; + for (let i = indices.length - 1; i > 0; i--) { + s = (s * 1103515245 + 12345) & 0x7fffffff; + const j = s % (i + 1); + [indices[i], indices[j]] = [indices[j], indices[i]]; + } + return indices.slice(0, count).map(i => pool[i]); +} + +async function judgeCluster(client, model, cluster, threadDetails) { + const items = cluster.memberThreadIds.map(id => { + const t = threadDetails.get(id); + if (!t) return ` - Thread ID ${id}: (details not found)`; + return ` - #${t.number} (${t.kind}): "${t.title}" — ${t.dedupeSummary || '(no summary)'}`; + }).join('\n'); + + const input = `Cluster with ${cluster.memberThreadIds.length} items:\n${items}`; + + const response = await client.responses.create({ + model, + input: [ + { role: 'system', content: [{ type: 'input_text', text: CLUSTER_RUBRIC }] }, + { role: 'user', content: [{ type: 'input_text', text: input }] }, + ], + text: { + format: { type: 'json_schema', name: 'cluster_judge', strict: true, schema: { + type: 'object', + properties: { + score: { type: 'integer' }, + distinct_topics: { type: 'integer' }, + outlier_count: { type: 'integer' }, + dominant_theme: { type: 'string' }, + reasoning: { type: 'string' }, + }, + required: ['score', 'distinct_topics', 'outlier_count', 'dominant_theme', 'reasoning'], + additionalProperties: false, + }}, + }, + max_output_tokens: 500, + }); + + try { + return JSON.parse(response.output_text ?? '{}'); + } catch { + return { score: null, reasoning: 'parse error' }; + } +} + +async function judgeSingleton(client, model, threadDetail) { + const input = `Thread #${threadDetail.number} (${threadDetail.kind}): "${threadDetail.title}"\ndedupe_summary: ${threadDetail.dedupeSummary || '(none)'}`; + + const response = await client.responses.create({ + model, + input: [ + { role: 'system', content: [{ type: 'input_text', text: SINGLETON_RUBRIC }] }, + { role: 'user', content: [{ type: 'input_text', text: input }] }, + ], + text: { + format: { type: 'json_schema', name: 'singleton_judge', strict: true, schema: { + type: 'object', + properties: { + score: { type: 'integer' }, + reasoning: { type: 'string' }, + }, + required: ['score', 'reasoning'], + additionalProperties: false, + }}, + }, + max_output_tokens: 300, + }); + + try { + return JSON.parse(response.output_text ?? '{}'); + } catch { + return { score: null, reasoning: 'parse error' }; + } +} + +// Main execution +const args = parseArgs(process.argv.slice(2)); + +const apiKey = process.env.OPENAI_API_KEY; +if (!apiKey) throw new Error('OPENAI_API_KEY not set'); +const client = new OpenAI({ apiKey }); + +const service = new GHCrawlService(); + +try { + // Step 1: Run clustering + process.stderr.write(`[experiment] ${args.experimentId}: running clustering...\n`); + const result = service.clusterExperiment({ + owner: args.owner, + repo: args.repo, + backend: 'vectorlite', + minScore: args.threshold, + k: args.k, + candidateK: args.candidateK, + efSearch: args.efSearch, + maxClusterSize: args.maxClusterSize, + clusterMode: args.clusterMode, + sourceKinds: args.sourceKinds, + aggregation: args.aggregation, + aggregationWeights: args.aggregationWeights, + includeClusters: true, + onProgress: (msg) => process.stderr.write(`${msg}\n`), + }); + + const totalThreads = result.threads; + const soloClusters = result.clusterSizes.soloClusters; + const multiMemberClusters = result.clusters - soloClusters; + const threadsInMulti = totalThreads - soloClusters; + const multiMemberPct = totalThreads > 0 ? threadsInMulti / totalThreads : 0; + + const metrics = { + multi_member_pct: Math.round(multiMemberPct * 10000) / 100, + edge_count: result.edges, + cluster_count: result.clusters, + solo_clusters: soloClusters, + multi_member_clusters: multiMemberClusters, + threads_in_multi: threadsInMulti, + total_threads: totalThreads, + max_cluster_size: result.clusterSizes.maxClusterSize, + solo_pct: Math.round((soloClusters / Math.max(result.clusters, 1)) * 10000) / 100, + avg_multi_size: multiMemberClusters > 0 ? Math.round((threadsInMulti / multiMemberClusters) * 100) / 100 : 0, + duration_ms: result.durationMs, + }; + + process.stderr.write(`[experiment] clustering done: ${metrics.multi_member_pct}% multi-member, ${metrics.edge_count} edges\n`); + + // Check degenerate gates + if (metrics.solo_pct >= 95 || metrics.max_cluster_size > 500 || metrics.multi_member_pct < 5) { + process.stderr.write(`[experiment] DEGENERATE: solo_pct=${metrics.solo_pct} max_cluster=${metrics.max_cluster_size} multi%=${metrics.multi_member_pct}\n`); + const output = { experiment_id: args.experimentId, outcome: 'degenerate', metrics, judge: null }; + fs.mkdirSync(path.resolve(args.outputDir), { recursive: true }); + fs.writeFileSync(path.resolve(args.outputDir, `${args.experimentId}.json`), JSON.stringify(output, null, 2)); + process.stdout.write(JSON.stringify({ experiment_id: args.experimentId, outcome: 'degenerate', ...metrics }, null, 2) + '\n'); + process.exit(0); + } + + // Step 2: Load thread details for judging + process.stderr.write(`[experiment] loading thread details for judging...\n`); + const clusters = result.clustersDetail; + const allThreadIds = new Set(); + for (const c of clusters) { + for (const id of c.memberThreadIds) allThreadIds.add(id); + } + + const threadDetails = new Map(); + const threadIds = Array.from(allThreadIds); + for (let i = 0; i < threadIds.length; i += 500) { + const batch = threadIds.slice(i, i + 500); + const placeholders = batch.map(() => '?').join(','); + const rows = service.db.prepare( + `select t.id, t.number, t.kind, t.title, s.summary_text as dedupe_summary + from threads t + left join document_summaries s on s.thread_id = t.id and s.summary_kind = 'dedupe_summary' + where t.id in (${placeholders})` + ).all(...batch); + for (const row of rows) { + threadDetails.set(row.id, { + number: row.number, + kind: row.kind, + title: row.title, + dedupeSummary: row.dedupe_summary, + }); + } + } + + // Step 3: Sample clusters + const { sampled, singletons } = sampleClusters(clusters, args.clusterSampleSize); + const singletonSample = deterministicSample(singletons, args.singletonSampleSize, 42); + + process.stderr.write(`[experiment] sampled ${sampled.length} clusters + ${singletonSample.length} singletons for judging\n`); + + // Step 4: Judge clusters + const clusterJudgments = []; + for (const [i, cluster] of sampled.entries()) { + process.stderr.write(`[judge] cluster ${i + 1}/${sampled.length} (size=${cluster.memberThreadIds.length}, bucket=${cluster.bucket})\n`); + const judgment = await judgeCluster(client, args.judgeModel, cluster, threadDetails); + clusterJudgments.push({ + bucket: cluster.bucket, + size: cluster.memberThreadIds.length, + representativeThreadId: cluster.representativeThreadId, + judgment, + }); + } + + // Step 5: Judge singletons + const singletonJudgments = []; + for (const [i, singleton] of singletonSample.entries()) { + const threadId = singleton.memberThreadIds[0]; + const detail = threadDetails.get(threadId); + if (!detail) continue; + process.stderr.write(`[judge] singleton ${i + 1}/${singletonSample.length} #${detail.number}\n`); + const judgment = await judgeSingleton(client, args.judgeModel, detail); + singletonJudgments.push({ + threadId, + number: detail.number, + title: detail.title, + judgment, + }); + } + + // Step 6: Aggregate + const scoredClusters = clusterJudgments.filter(j => j.judgment?.score != null); + const meanScore = scoredClusters.length > 0 + ? scoredClusters.reduce((s, j) => s + j.judgment.score, 0) / scoredClusters.length + : 0; + const meanDistinctTopics = scoredClusters.length > 0 + ? scoredClusters.reduce((s, j) => s + (j.judgment.distinct_topics ?? 0), 0) / scoredClusters.length + : 0; + const totalOutliers = scoredClusters.reduce((s, j) => s + (j.judgment.outlier_count ?? 0), 0); + const totalMembers = scoredClusters.reduce((s, j) => s + j.size, 0); + const outlierRate = totalMembers > 0 ? totalOutliers / totalMembers : 0; + + const scoredSingletons = singletonJudgments.filter(j => j.judgment?.score != null); + const singletonScore = scoredSingletons.length > 0 + ? scoredSingletons.reduce((s, j) => s + j.judgment.score, 0) / scoredSingletons.length + : 0; + + // Per-bucket breakdown + const bucketScores = {}; + for (const bucket of ['top_by_size', 'mid_range', 'small_clusters']) { + const bucketItems = scoredClusters.filter(j => j.bucket === bucket); + bucketScores[bucket] = bucketItems.length > 0 + ? Math.round(bucketItems.reduce((s, j) => s + j.judgment.score, 0) / bucketItems.length * 100) / 100 + : null; + } + + const judgeResults = { + mean_score: Math.round(meanScore * 100) / 100, + mean_distinct_topics: Math.round(meanDistinctTopics * 100) / 100, + outlier_rate: Math.round(outlierRate * 10000) / 100, + singleton_score: Math.round(singletonScore * 100) / 100, + bucket_scores: bucketScores, + clusters_judged: scoredClusters.length, + singletons_judged: scoredSingletons.length, + }; + + // Save full results + const output = { + experiment_id: args.experimentId, + outcome: 'measured', + timestamp: new Date().toISOString(), + params: { + source_kinds: args.sourceKinds ?? 'all', + aggregation: args.aggregation ?? 'max', + threshold: args.threshold ?? 0.82, + k: args.k ?? 6, + max_cluster_size: args.maxClusterSize, + cluster_mode: args.clusterMode, + }, + metrics, + judge: judgeResults, + cluster_judgments: clusterJudgments, + singleton_judgments: singletonJudgments, + }; + + fs.mkdirSync(path.resolve(args.outputDir), { recursive: true }); + const outputPath = path.resolve(args.outputDir, `${args.experimentId}.json`); + fs.writeFileSync(outputPath, JSON.stringify(output, null, 2)); + process.stderr.write(`\n[experiment] results saved to ${outputPath}\n`); + + // Print summary to stdout + process.stdout.write(JSON.stringify({ + experiment_id: args.experimentId, + outcome: 'measured', + ...metrics, + ...judgeResults, + }, null, 2) + '\n'); +} finally { + service.close(); +} diff --git a/scripts/run-cluster-experiments.mjs b/scripts/run-cluster-experiments.mjs new file mode 100644 index 0000000..9bbf8c2 --- /dev/null +++ b/scripts/run-cluster-experiments.mjs @@ -0,0 +1,99 @@ +#!/usr/bin/env node +/** + * Run all clustering experiments sequentially. + * Usage: node scripts/op-run.mjs run -- node scripts/run-cluster-experiments.mjs + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const resultsDir = path.join(repoRoot, '.context', 'compound-engineering', 'ce-optimize', 'embedding-clustering', 'results'); + +const EXPERIMENTS = [ + // Baseline: all 3 source kinds, max aggregation + { id: 'baseline-all-max', args: ['--aggregation', 'max'] }, + + // Source selection experiments + { id: 'source-dedupe-only', args: ['--source-kinds', 'dedupe_summary', '--aggregation', 'max'] }, + { id: 'source-title-dedupe', args: ['--source-kinds', 'title,dedupe_summary', '--aggregation', 'max'] }, + { id: 'source-body-dedupe', args: ['--source-kinds', 'body,dedupe_summary', '--aggregation', 'max'] }, + + // Aggregation method experiments (all 3 source kinds) + { id: 'agg-mean', args: ['--aggregation', 'mean'] }, + { id: 'agg-weighted', args: ['--aggregation', 'weighted'] }, + { id: 'agg-weighted-heavy-summary', args: ['--aggregation', 'weighted', '--weights', '{"dedupe_summary":0.7,"title":0.2,"body":0.1}'] }, + { id: 'agg-min-of-2', args: ['--aggregation', 'min-of-2'] }, + { id: 'agg-boost', args: ['--aggregation', 'boost'] }, + + // Parameter tuning (using dedupe_summary only, which is likely cleanest signal) + { id: 'param-low-threshold', args: ['--source-kinds', 'dedupe_summary', '--threshold', '0.75'] }, + { id: 'param-high-threshold', args: ['--source-kinds', 'dedupe_summary', '--threshold', '0.88'] }, + { id: 'param-more-neighbors', args: ['--source-kinds', 'dedupe_summary', '--k', '12'] }, + { id: 'param-large-clusters', args: ['--source-kinds', 'dedupe_summary', '--max-cluster-size', '400'] }, + + // Best combos (will add based on early results) + { id: 'combo-dedupe-weighted-low', args: ['--source-kinds', 'title,dedupe_summary', '--aggregation', 'weighted', '--threshold', '0.78'] }, + { id: 'combo-all-boost-low', args: ['--aggregation', 'boost', '--threshold', '0.78'] }, +]; + +// Check which experiments already have results +const existing = new Set( + fs.existsSync(resultsDir) + ? fs.readdirSync(resultsDir).filter(f => f.endsWith('.json')).map(f => f.replace('.json', '')) + : [] +); + +const summaryTable = []; +const commonArgs = [ + 'openclaw/openclaw', + '--cluster-mode', 'bounded', + '--max-cluster-size', '200', +]; + +for (const experiment of EXPERIMENTS) { + if (existing.has(experiment.id)) { + try { + const result = JSON.parse(fs.readFileSync(path.join(resultsDir, `${experiment.id}.json`), 'utf8')); + if (result.judge?.mean_score != null) { + process.stderr.write(`[SKIP] ${experiment.id} — already has judge results\n`); + summaryTable.push({ experiment_id: experiment.id, ...result.metrics, ...result.judge, status: 'cached' }); + continue; + } + } catch { /* rerun */ } + } + + process.stderr.write(`\n=== Running ${experiment.id} ===\n`); + + // Override max-cluster-size if the experiment specifies it + const expArgs = [...experiment.args]; + const hasMaxCluster = expArgs.includes('--max-cluster-size'); + + try { + const allArgs = [ + path.join(repoRoot, 'scripts', 'cluster-judge-experiment.mjs'), + ...commonArgs, + '--experiment-id', experiment.id, + ...(hasMaxCluster ? [] : ['--max-cluster-size', '200']), + ...expArgs, + ]; + + const stdout = execFileSync('node', allArgs, { + cwd: repoRoot, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'inherit'], + timeout: 600_000, + env: process.env, + }); + + const result = JSON.parse(stdout.trim()); + summaryTable.push({ ...result, status: 'completed' }); + } catch (error) { + process.stderr.write(`[ERROR] ${experiment.id}: ${error.message}\n`); + summaryTable.push({ experiment_id: experiment.id, status: 'error', error: error.message }); + } +} + +process.stderr.write('\n\n=== SUMMARY TABLE ===\n'); +process.stdout.write(JSON.stringify(summaryTable, null, 2) + '\n'); From 4dd92e7234b14100d0c87a0a00d841456b0d5a1d Mon Sep 17 00:00:00 2001 From: huntharo Date: Tue, 31 Mar 2026 15:37:52 -0400 Subject: [PATCH 20/34] fix: improve cluster judge reliability and add backend arg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add --backend flag to cluster-judge-experiment.mjs (supports exact/vectorlite) - Increase max_output_tokens for cluster judge (500→800) and singleton judge (300→500) - Truncate large clusters to 25 representative items for judge context limits - Add .context/ to .gitignore Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + scripts/cluster-judge-experiment.mjs | 29 +++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index ba90e8d..4fca5e8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ packages/*/src/**/*.d.ts.map **/__pycache__/ **/*.pyc **/.DS_Store +.context/ diff --git a/scripts/cluster-judge-experiment.mjs b/scripts/cluster-judge-experiment.mjs index d89f33b..f44d286 100644 --- a/scripts/cluster-judge-experiment.mjs +++ b/scripts/cluster-judge-experiment.mjs @@ -68,6 +68,7 @@ function parseArgs(argv) { let k; let candidateK; let efSearch; + let backend = 'vectorlite'; let maxClusterSize = 200; let clusterMode = 'bounded'; let clusterSampleSize = 30; @@ -86,6 +87,7 @@ function parseArgs(argv) { if (token === '--k') { k = Number(argv[++index]); continue; } if (token === '--candidate-k') { candidateK = Number(argv[++index]); continue; } if (token === '--ef-search') { efSearch = Number(argv[++index]); continue; } + if (token === '--backend') { backend = argv[++index]; continue; } if (token === '--max-cluster-size') { maxClusterSize = Number(argv[++index]); continue; } if (token === '--cluster-mode') { clusterMode = argv[++index]; continue; } if (token === '--cluster-sample-size') { clusterSampleSize = Number(argv[++index]); continue; } @@ -98,6 +100,7 @@ function parseArgs(argv) { return { owner, repo: name, experimentId, outputDir, + backend, sourceKinds, aggregation, aggregationWeights, threshold, k, candidateK, efSearch, maxClusterSize, clusterMode, @@ -151,13 +154,29 @@ function deterministicSample(pool, count, seed) { } async function judgeCluster(client, model, cluster, threadDetails) { - const items = cluster.memberThreadIds.map(id => { + // For large clusters, show a sample of items to avoid exceeding context limits + let displayIds = cluster.memberThreadIds; + let truncationNote = ''; + if (displayIds.length > 25) { + // Show first 10, last 5, and 10 evenly spaced from the middle + const first = displayIds.slice(0, 10); + const last = displayIds.slice(-5); + const middle = []; + const step = Math.floor((displayIds.length - 15) / 10); + for (let i = 10; i < displayIds.length - 5 && middle.length < 10; i += Math.max(1, step)) { + middle.push(displayIds[i]); + } + displayIds = [...first, ...middle, ...last]; + truncationNote = `\n(Showing ${displayIds.length} of ${cluster.memberThreadIds.length} items — sampled for brevity)`; + } + + const items = displayIds.map(id => { const t = threadDetails.get(id); if (!t) return ` - Thread ID ${id}: (details not found)`; return ` - #${t.number} (${t.kind}): "${t.title}" — ${t.dedupeSummary || '(no summary)'}`; }).join('\n'); - const input = `Cluster with ${cluster.memberThreadIds.length} items:\n${items}`; + const input = `Cluster with ${cluster.memberThreadIds.length} items:${truncationNote}\n${items}`; const response = await client.responses.create({ model, @@ -179,7 +198,7 @@ async function judgeCluster(client, model, cluster, threadDetails) { additionalProperties: false, }}, }, - max_output_tokens: 500, + max_output_tokens: 800, }); try { @@ -209,7 +228,7 @@ async function judgeSingleton(client, model, threadDetail) { additionalProperties: false, }}, }, - max_output_tokens: 300, + max_output_tokens: 500, }); try { @@ -234,7 +253,7 @@ try { const result = service.clusterExperiment({ owner: args.owner, repo: args.repo, - backend: 'vectorlite', + backend: args.backend, minScore: args.threshold, k: args.k, candidateK: args.candidateK, From ffd31f5677249bfb4c2fc0dfc8632a89f142ebfe Mon Sep 17 00:00:00 2001 From: huntharo Date: Tue, 31 Mar 2026 15:52:30 -0400 Subject: [PATCH 21/34] fix: handle missing clusterExperiment in CI base worktree The CI perf harness copies HEAD's perf.integration.ts into the base worktree, but clusterExperiment doesn't exist on main. Add a runtime guard that falls back to clusterRepository when the method is missing. Also regenerate lockfile after rebase. Co-Authored-By: Claude Opus 4.6 --- .../api-core/src/cluster/perf.integration.ts | 27 ++++++++++- pnpm-lock.yaml | 47 ++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/api-core/src/cluster/perf.integration.ts b/packages/api-core/src/cluster/perf.integration.ts index 7b48698..f738d36 100644 --- a/packages/api-core/src/cluster/perf.integration.ts +++ b/packages/api-core/src/cluster/perf.integration.ts @@ -354,6 +354,31 @@ async function runSingleCluster( }> { const service = createService(dbPath); try { + // clusterExperiment may not exist on older branches (e.g. base worktree in CI) + if (typeof service.clusterExperiment !== 'function') { + const startedAt = performance.now(); + const result = await service.clusterRepository({ + owner: 'openclaw', + repo: 'openclaw', + k: baseline.fixture.k, + minScore: baseline.fixture.minScore, + }); + const durationMs = performance.now() - startedAt; + return { + durationMs, + totalDurationMs: durationMs, + loadMs: 0, + setupMs: 0, + edgeBuildMs: durationMs, + indexBuildMs: 0, + queryMs: 0, + clusterBuildMs: 0, + peakRssBytes: 0, + peakHeapUsedBytes: 0, + clusters: result.clusters, + edges: result.edges, + }; + } const result = service.clusterExperiment({ owner: 'openclaw', repo: 'openclaw', @@ -523,7 +548,7 @@ function buildSummary(result: PerfRunResult): string { const status = result.deltaPercent > result.maxRegressionPercent ? 'FAIL' : 'PASS'; const sampleList = result.sampleDurationsMs.map((value) => formatDurationMs(value)).join(', '); const suggestedBaseline = buildSuggestedBaseline(result); - const timingLabel = result.timingBasis === 'post-index' ? 'Fixture median (post-index)' : 'Fixture median'; + const timingLabel = 'Fixture median'; const bootstrapLine = result.baselineMedianMs === result.medianMs ? '- Bootstrap mode: using the current fixture median as the provisional baseline' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6f15cb..2127129 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,7 +69,7 @@ importers: specifier: ^1.1.2 version: 1.1.2(aggregate-error@3.1.0) better-sqlite3: - specifier: ^12.8.0 + specifier: ^12.2.0 version: 12.8.0 dotenv: specifier: ^17.2.2 @@ -80,12 +80,35 @@ importers: openai: specifier: ^6.33.0 version: 6.33.0(zod@4.3.6) + vectorlite: + specifier: ^0.2.0 + version: 0.2.0 zod: specifier: ^4.3.6 version: 4.3.6 packages: + '@1yefuwang1/vectorlite-darwin-arm64@0.2.0': + resolution: {integrity: sha512-taYA4xt4zFgQi0DwZvS1pr0bYmP7uAYVScxFGf5rl3CIxOXDxPyHytHRrvU5rqPUprLZlnxEb/gNGvCplYLNhA==} + cpu: [arm64] + os: [darwin] + + '@1yefuwang1/vectorlite-darwin-x64@0.2.0': + resolution: {integrity: sha512-vOU7h+PPE7VoJkUw6UQ2IT+a4Stzgj103yKdr85iLJqi4oX8TjxNH2OP5c1LKU8ZT/vQziKAqUE+p63XmFP8LA==} + cpu: [x64] + os: [darwin] + + '@1yefuwang1/vectorlite-linux-x64@0.2.0': + resolution: {integrity: sha512-VWgE4DVPVKoVzStnUR+PASWUloi5mUO+z7TEfsu6zHEBzPkOgL9ofC0DDr9/rFo1UNozN93j17ZOZL0HTsZKcQ==} + cpu: [x64] + os: [linux] + + '@1yefuwang1/vectorlite-win32-x64@0.2.0': + resolution: {integrity: sha512-NLZLfxQf2wS+PrhL1bavrULOSQhaOItGVsLebKy0Tu8Hpn81M3KPmweKwNiVTSqfLzy0IlhSalLlb+ypUdRm0Q==} + cpu: [x64] + os: [win32] + '@clack/core@1.1.0': resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==} @@ -590,6 +613,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vectorlite@0.2.0: + resolution: {integrity: sha512-hHFAISJuUblqTecD/EtNmEhIm4P6vTax4tswN486qCBDtse9uqBPLGdUsW4+CSjyG9Zoc0Jxj+dubQSZjvYGqg==} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -603,6 +629,18 @@ packages: snapshots: + '@1yefuwang1/vectorlite-darwin-arm64@0.2.0': + optional: true + + '@1yefuwang1/vectorlite-darwin-x64@0.2.0': + optional: true + + '@1yefuwang1/vectorlite-linux-x64@0.2.0': + optional: true + + '@1yefuwang1/vectorlite-win32-x64@0.2.0': + optional: true + '@clack/core@1.1.0': dependencies: sisteransi: 1.0.5 @@ -1090,6 +1128,13 @@ snapshots: util-deprecate@1.0.2: {} + vectorlite@0.2.0: + optionalDependencies: + '@1yefuwang1/vectorlite-darwin-arm64': 0.2.0 + '@1yefuwang1/vectorlite-darwin-x64': 0.2.0 + '@1yefuwang1/vectorlite-linux-x64': 0.2.0 + '@1yefuwang1/vectorlite-win32-x64': 0.2.0 + wrappy@1.0.2: {} yaml@2.8.3: {} From 007b85712ff68315070481e6b94171a0786a232c Mon Sep 17 00:00:00 2001 From: huntharo Date: Tue, 31 Mar 2026 23:16:01 -0400 Subject: [PATCH 22/34] fix: correct parseRepoFlags arity and writeProgress signature in cluster-experiment CLI Co-Authored-By: Claude Opus 4.6 --- apps/cli/src/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts index 928b2ef..b00b84b 100644 --- a/apps/cli/src/main.ts +++ b/apps/cli/src/main.ts @@ -938,7 +938,7 @@ export async function run( } } case 'cluster-experiment': { - const { owner, repo, values } = parseRepoFlags(rest); + const { owner, repo, values } = parseRepoFlags('cluster-experiment', rest); const backend = values.backend === 'exact' || values.backend === 'vectorlite' ? values.backend : undefined; const result = getService().clusterExperiment({ owner, @@ -947,7 +947,7 @@ export async function run( k: typeof values.k === 'string' ? Number(values.k) : undefined, minScore: typeof values.threshold === 'string' ? Number(values.threshold) : undefined, candidateK: typeof values['candidate-k'] === 'string' ? Number(values['candidate-k']) : undefined, - onProgress: writeProgress, + onProgress: (message: string) => writeProgress(message, stderr), }); stdout.write(`${JSON.stringify(result, null, 2)}\n`); return; From 52009b3ff365ec975ecffff42364141419e8f7e5 Mon Sep 17 00:00:00 2001 From: huntharo Date: Wed, 1 Apr 2026 09:43:12 -0400 Subject: [PATCH 23/34] docs: add clustering optimization results and recommendations Documents the embedding clustering optimization experiments comparing exact kNN vs vectorlite HNSW, source kind selection, and aggregation strategies. Records why source-dedupe-only was chosen as the recommended configuration. Co-Authored-By: Claude Opus 4.6 --- ...-hnsw-embedding-optimization-2026-03-30.md | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/solutions/performance-issues/clustering-vectorlite-hnsw-embedding-optimization-2026-03-30.md diff --git a/docs/solutions/performance-issues/clustering-vectorlite-hnsw-embedding-optimization-2026-03-30.md b/docs/solutions/performance-issues/clustering-vectorlite-hnsw-embedding-optimization-2026-03-30.md new file mode 100644 index 0000000..78151df --- /dev/null +++ b/docs/solutions/performance-issues/clustering-vectorlite-hnsw-embedding-optimization-2026-03-30.md @@ -0,0 +1,116 @@ +--- +title: "Clustering optimization: dedupe_summary embeddings with vectorlite HNSW outperform exact kNN" +date: 2026-03-30 +category: performance-issues +module: clustering +problem_type: performance_issue +component: tooling +symptoms: + - "Original exact kNN clustering produced ungoverned 455-member mega-clusters" + - "Only 31.2% of threads ended up in multi-member clusters" + - "Raw title+body embeddings included template boilerplate noise" +root_cause: missing_tooling +resolution_type: tooling_addition +severity: medium +tags: + - clustering + - embeddings + - vectorlite + - hnsw + - llm-as-judge + - prompt-optimization + - dedupe-summary +--- + +# Clustering optimization: dedupe_summary embeddings with vectorlite HNSW outperform exact kNN + +## Problem + +GitCrawl clusters ~18,500 GitHub issues/PRs by embedding similarity to identify duplicates and related threads. The original pipeline used exact k-nearest-neighbor search on title+body embeddings with unbounded clustering. This produced 4.73/5 coherence but ungoverned cluster growth (max 455 members), only 31.2% coverage, and 1.70% outlier rate. The goal was to improve coherence, control cluster sizes, and increase coverage. + +## Approach + +### Phase 1: Summarization Prompt Optimization + +Tested 11 prompt variants for summarizing issue/PR content before embedding. An LLM-as-judge (gpt-5-mini) scored each on boilerplate removal, signal density, and clustering suitability (1-5 scale). + +**Winner: `v5-component-focused` (4.97/5 vs baseline 2.65/5).** Key insight: explicit component-first structure (e.g., "Discord gateway: connection drops on resume") clusters far better than generic summaries. Full summarization of 18.5k threads cost $26 using gpt-5.4-mini. + +### Phase 2: Clustering Experiments + +Sixteen configurations tested across four dimensions: + +- **Embedding sources**: title, body, dedupe_summary (optimized summaries), and combinations +- **Search backend**: Vectorlite HNSW approximate nearest-neighbor vs exact kNN +- **Score aggregation**: max, mean, weighted, min-of-2, boost +- **Parameters**: similarity threshold (0.75-0.88), neighbor count k (6, 12), max cluster size (200, 400) + +Clustering used **size-bounded Union-Find**: edges sorted by descending score, merges refused when exceeding maxSize cap. + +**Evaluation** used LLM-as-judge with stratified sampling: 30 clusters (10 large, 10 mid, 10 small) scored for coherence, plus 15 singletons scored for false-negative detection. + +## Results + +### Baselines + +| Configuration | Sources | Backend | Mode | Coherence | Multi% | MaxSz | AvgSz | Outlier% | Duration | +|---|---|---|---|---|---|---|---|---|---| +| **Original** | title+body | exact kNN | basic (unbounded) | 4.73 | 31.2% | 455 | 3.52 | 1.70% | 800s | +| All sources, max agg | all 3 | vectorlite | bounded (200) | 4.62 | 49.5% | 200 | 4.64 | 2.07% | 180s | + +### Key Experiments (vectorlite HNSW, bounded mode, maxSize=200) + +| Experiment | Sources | Aggregation | Threshold | Coherence | Multi% | Outlier% | Takeaway | +|---|---|---|---|---|---|---|---| +| **source-dedupe-only** | dedupe_summary | max | 0.82 | **4.93** | 44.6% | **0.85%** | **Recommended.** Best coherence at reasonable coverage. | +| agg-min-of-2 | all 3 | min-of-2 | 0.82 | 4.97 | 23.8% | 0.67% | Highest coherence but low coverage. Precision champion. | +| param-high-threshold | all 3 | max | 0.88 | 5.00 | 14.4% | 0.00% | Perfect coherence, too conservative for general use. | +| agg-boost | all 3 | boost | 0.82 | 4.85 | 49.5% | 0.94% | Best multi-source option if more coverage needed. | +| source-body-dedupe | body+dedupe | max | 0.82 | 4.89 | 48.8% | 1.47% | Adding body helps coverage slightly, hurts coherence. | +| param-low-threshold | dedupe_summary | max | 0.75 | 4.77 | 77.4% | 0.96% | High coverage but coherence drops and clusters get large (avg 11.7). | +| baseline-all-max | all 3 | max | 0.82 | 4.62 | 49.5% | 2.07% | Adding dedupe_summary to max agg made things *worse*. | + +### What the Columns Mean + +- **Coherence** (1-5): LLM judge score for how well cluster members relate. Stratified sample of 30 clusters. +- **Multi%**: Percentage of threads in multi-member clusters (coverage). +- **Outlier%**: Percentage of cluster members judged as not belonging. + +## Recommended Configuration + +**`source-dedupe-only` with Vectorlite HNSW, threshold 0.82, maxSize 200.** + +- **+0.20 coherence** over original (4.93 vs 4.73) +- **+13% coverage** (44.6% vs 31.2% multi-member) +- **Half the outlier rate** (0.85% vs 1.70%) +- **15x faster** (55s vs 800s) +- **Simplest**: single embedding source, no aggregation complexity +- **Controlled cluster sizes**: max 200 vs unbounded 455 + +## Key Learnings + +1. **Summarization prompt quality is the biggest lever.** The prompt improvement (2.65 to 4.97 judge score) drove more quality gain than any clustering algorithm change. Good embeddings matter more than clever aggregation. + +2. **More sources does not mean better clusters.** Naive multi-source max aggregation (4.62) was *worse* than single-source dedupe-summary (4.93). Title and body embeddings introduce noise that dilutes the optimized summary signal. + +3. **Multi-source only helps with strict aggregation.** The only multi-source configs that beat single-source used min-of-2 or high thresholds -- essentially filtering out noise from weaker sources. Added complexity for marginal gain. + +4. **HNSW approximate search outperforms exact kNN in practice.** The approximate search found ~2x more edges because it casts a wider net. This produced better clusters, not worse, while being 15x faster. + +5. **Size-bounded Union-Find is essential.** The original system's largest cluster (455 members) was incoherent. Capping at 200 with score-ordered merging ensures best edges are used first. + +6. **Mid-range and small clusters are consistently perfect (5.0).** Quality issues concentrate in the largest clusters. The top_by_size bucket is the discriminator between configs. + +## Future Work + +- **Threshold tuning per component**: Different areas may cluster at different similarity levels +- **Hierarchical clustering**: Tight clusters first (0.88), then looser grouping (0.78) for topic organization +- **Coverage gap analysis**: 55.4% of threads remain singletons -- sampling these would quantify false-negative rate +- **Incremental updates**: Delta-based matching against existing cluster centroids instead of full rebuild + +## Related + +- `docs/DESIGN.md` -- Original architecture describing exact cosine similarity kNN approach +- `docs/PLAN.md` -- Phase 4 (Embeddings) and Phase 5 (OpenSearch Evaluation) +- `.context/compound-engineering/ce-optimize/embedding-clustering/` -- Raw experiment results (16 JSON files) +- `.context/compound-engineering/ce-optimize/summary-prompt/` -- Prompt optimization results (11 variants) From e1f8b7667d5c320c8c6d9e46be1ec56e72400d27 Mon Sep 17 00:00:00 2001 From: huntharo Date: Wed, 1 Apr 2026 18:37:53 -0400 Subject: [PATCH 24/34] docs: add vectorlite migration release brainstorm --- ...arch-and-summary-migration-requirements.md | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 docs/brainstorms/2026-04-01-vectorlite-default-search-and-summary-migration-requirements.md diff --git a/docs/brainstorms/2026-04-01-vectorlite-default-search-and-summary-migration-requirements.md b/docs/brainstorms/2026-04-01-vectorlite-default-search-and-summary-migration-requirements.md new file mode 100644 index 0000000..ed7b5bb --- /dev/null +++ b/docs/brainstorms/2026-04-01-vectorlite-default-search-and-summary-migration-requirements.md @@ -0,0 +1,124 @@ +--- +date: 2026-04-01 +topic: vectorlite-default-search-and-summary-migration +--- + +# Vectorlite Default Search And Summary Migration + +## Problem Frame + +`ghcrawl` currently treats `vectorlite` as an experiment, keeps embeddings in `document_embeddings.embedding_json`, and rebuilds clusters from in-memory exact similarity logic. That creates three problems: + +1. Clustering and semantic lookups do not scale cleanly as repos grow past 10k-20k issues/PRs. +2. The product does not yet have a persistent ANN index that can answer semantic search or cluster-membership questions quickly for day-to-day use. +3. The current embedding/summarization pipeline is not versioned strongly enough to support a deliberate migration to shorter embeddings, a persistent vector index, and user-selectable summary models without confusion about what is stale. + +The release needs to move `ghcrawl` to a persistent `vectorlite`-backed search model, migrate embeddings to 1024-dimensional `text-embedding-3-large`, preserve summary-skip behavior for unchanged inputs, and make the operator experience clear about cost, migration state, and rebuild behavior. + +```mermaid +flowchart TB + A["User upgrades ghcrawl"] --> B["doctor / configure shows migration state"] + B --> C["User runs refresh"] + C --> D["sync GitHub metadata"] + D --> E["summarize only stale threads when summary-based embeddings are enabled"] + E --> F["rebuild embeddings at 1024 dimensions"] + F --> G["write/update persistent vectorlite index"] + G --> H["rebuild clusters from vectorlite search"] + H --> I["mark repo migrated and ready for semantic search"] +``` + +## Requirements + +**Release Structure** +- R1. Ship this as a single coordinated breaking release rather than splitting it across three releases. +- R2. Treat `vectorlite` as a runtime requirement for supported installs in this release, not an experiment-only optional dependency. +- R3. Promote vector-backed semantic search and clustering to the supported default path for day-to-day operation. + +**Persistent Vector Search** +- R4. `ghcrawl` must maintain a persistent `vectorlite`-backed vector index for each repository so semantic lookup does not require loading the full embedding corpus into memory. +- R5. Semantic search must query the persistent vector index directly rather than the current exact in-memory scan path. +- R6. The product must support fast “find likely cluster membership / nearest neighbors for a newly synced thread” using the persistent vector index. +- R7. The release must define one supported persistence strategy for the vector index and keep it stable for the release. The recommended default is a managed sidecar index or sidecar SQLite store rather than storing `vectorlite` virtual tables in the canonical issue/PR database. + +**Summarization And Embedding Pipeline** +- R8. The release must support two summarization models for operator choice: + - `gpt-5-mini` + - `gpt-5.4-mini` +- R9. The default summarization model for new and upgraded installs must be `gpt-5-mini`. +- R10. The release must continue skipping summarization work when the summary input has not changed, and automated tests must prove this behavior. +- R11. The release must move embeddings to `text-embedding-3-large` with explicit `dimensions=1024`. +- R12. The release must store only one active embedding per thread for the active embedding basis, not multiple long-lived parallel embedding sources for old and new strategies. +- R13. The active embedding basis must be operator-selectable between: + - title + original description + - title + summarized description +- R14. The default embedding basis for this release should be title + summarized description because recent repo experiments indicate better clustering quality from the dedupe-summary path. +- R15. The stored embedding metadata must record the embedding basis and pipeline version used to create it so stale rows can be detected deterministically after config or model changes. +- R16. If the operator changes summarization model or embedding basis, `ghcrawl` must mark the affected summaries and/or embeddings stale instead of silently treating old data as current. + +**Refresh And Migration Behavior** +- R17. The first `refresh` after upgrade must detect that pre-release embeddings are obsolete and rebuild embeddings before vector search and clustering are treated as current. +- R18. If the active embedding basis depends on summaries, `refresh` must become summary-aware and run a summarize phase before embedding whenever the relevant summary content is missing or stale. +- R19. Existing cluster runs built from pre-migration embeddings must be treated as stale after upgrade and must not continue to appear as if they are current once the repo is known to need migration. +- R20. The operator experience must make migration status obvious before and during the first post-upgrade rebuild. +- R21. Rebuild behavior must be repository-scoped, so one repo can complete migration without forcing all repos to migrate immediately. +- R22. If a repo has not completed the required rebuild yet, commands that depend on current vectors or current clusters must say so clearly instead of returning misleading old results. + +**Operator Controls And UX** +- R23. Add a first-class `configure` command that shows the selected summarization model, embedding basis, vector backend status, and whether the current repo data is migrated or stale. +- R24. `configure` must allow the operator to switch summarization models intentionally. +- R25. The operator-facing docs and CLI help must explain that `gpt-5-mini` is the cheaper default and `gpt-5.4-mini` is the higher-quality, more expensive option. +- R26. `doctor` should report whether `vectorlite` loads successfully on the current machine, because this release makes it a supported runtime dependency. + +**Cost And Spend Transparency** +- R27. The docs must include an estimate, using April 1, 2026 USD pricing, for summarizing roughly 20k `openclaw/openclaw` issues/PRs with both supported summary models. +- R28. The release should communicate that a one-time full summarize of ~20k threads is expected to cost roughly: + - about `$30` with `gpt-5.4-mini` + - about `$11-$13` with `gpt-5-mini` + based on current OpenAI pricing and the repo’s observed token profile. +- R29. Long-running summarize/refresh progress output should continue reporting spend and estimated total cost so operators can stop early if needed. + +**Validation And Release Safety** +- R30. Tests must prove summary skipping still works when thread input is unchanged after this migration. +- R31. Tests must prove stale detection for summary model changes, embedding basis changes, and pre-migration embeddings. +- R32. Tests must prove semantic search uses the persistent vector index successfully after migration. +- R33. Release verification must include upgrade testing from a pre-vectorlite database to the new release on at least one realistic repo dataset. +- R34. Release verification must include packaging/install validation for the supported desktop/server platforms because `vectorlite` becomes a hard dependency. + +## Success Criteria +- Operators can upgrade and complete the first repo migration with a normal `refresh` flow instead of manually orchestrating summarize/embed/cluster recovery steps. +- Large repos no longer require loading the full embedding corpus into RAM for normal semantic lookup or cluster rebuild workflows. +- Semantic search and cluster-neighbor lookup are fast enough to feel interactive on migrated repos. +- Unchanged threads are not re-summarized on repeated refreshes. +- Docs and CLI clearly communicate model choice, migration status, and likely one-time spend before operators trigger a full rebuild. + +## Scope Boundaries +- Not in scope for this release: keeping exact in-memory search as a co-equal supported production path. +- Not in scope for this release: shipping three separate rollout releases for vector backend, embedding migration, and summary model controls. +- Not in scope for this release: introducing a web UI as part of the migration. +- Not in scope for this release: preserving old cluster results as “current” after the release knows the repo must be re-embedded. + +## Key Decisions +- One coordinated breaking release: The migration behavior is too coupled across vectors, summaries, search, and clustering to justify stretching it across three separate releases. +- `vectorlite` is a real dependency now: the release should treat it as supported runtime infrastructure, not an experiment hiding behind a side command. +- Default summary model is `gpt-5-mini`: it keeps the out-of-the-box one-time migration cost materially lower while preserving a higher-quality paid-up option. +- Default embedding basis is title + summarized description: recent repo experiments indicate this produces cleaner clusters than title/body alone. +- Existing summary skip behavior stays: the release should migrate the pipeline without turning `refresh` into an always-re-summarize money sink. + +## Dependencies / Assumptions +- The supported `vectorlite` packaging story is good enough on the platforms `ghcrawl` intends to support in this release. +- OpenAI pricing as of April 1, 2026 remains: + - `gpt-5-mini`: $0.25 / 1M input, $0.025 / 1M cached input, $2.00 / 1M output + - `gpt-5.4-mini`: $0.75 / 1M input, $0.075 / 1M cached input, $4.50 / 1M output +- The recent repo experiment result in `docs/solutions/performance-issues/clustering-vectorlite-hnsw-embedding-optimization-2026-03-30.md` remains directionally valid for the release default. + +## Outstanding Questions + +### Deferred To Planning +- [Affects R7][Technical] Should the persistent vector index live in a sidecar SQLite DB, a sidecar vector index file, or inside the primary DB schema with `vectorlite` virtual tables? +- [Affects R18][Technical] How should `refresh` expose the new summarize-aware phase in progress output and skip flags without making the CLI surface confusing? +- [Affects R19][Technical] What is the cleanest way to mark old cluster runs invalid after upgrade while still letting the app explain why clusters are unavailable? +- [Affects R23][Technical] Should `configure` remain fully interactive, accept flags for non-interactive use, or support both? +- [Affects R31][Needs research] What is the smallest durable pipeline-versioning scheme that covers summary model, summary prompt version, embedding basis, embedding model, and dimensions without becoming hard to maintain? + +## Next Steps +→ `/prompts:ce-plan` for structured implementation planning From 63f2ab4e08ef22758bab15dc1aefc1c198383755 Mon Sep 17 00:00:00 2001 From: huntharo Date: Wed, 1 Apr 2026 18:53:49 -0400 Subject: [PATCH 25/34] docs: add persistent vectorlite migration plan --- ...at-persistent-vectorlite-migration-plan.md | 479 ++++++++++++++++++ 1 file changed, 479 insertions(+) create mode 100644 docs/plans/2026-04-01-001-feat-persistent-vectorlite-migration-plan.md diff --git a/docs/plans/2026-04-01-001-feat-persistent-vectorlite-migration-plan.md b/docs/plans/2026-04-01-001-feat-persistent-vectorlite-migration-plan.md new file mode 100644 index 0000000..b01802a --- /dev/null +++ b/docs/plans/2026-04-01-001-feat-persistent-vectorlite-migration-plan.md @@ -0,0 +1,479 @@ +--- +title: feat: adopt persistent vectorlite search and summary-aware embedding migration +type: feat +status: active +date: 2026-04-01 +origin: docs/brainstorms/2026-04-01-vectorlite-default-search-and-summary-migration-requirements.md +--- + +# feat: adopt persistent vectorlite search and summary-aware embedding migration + +## Overview + +This plan upgrades `ghcrawl` from exact in-memory embedding usage to persistent `vectorlite`-backed search and clustering, while simultaneously migrating the embedding pipeline to a single active vector per thread using `text-embedding-3-large` at 1024 dimensions. The same release also formalizes operator choice of summarization model, makes `refresh` summary-aware when the active embedding basis depends on summaries, and preserves summary skip behavior so ongoing refreshes do not become an unnecessary spend multiplier. + +## Problem Frame + +The current product has a split personality: the repo now has a promising `vectorlite` experiment, but production behavior still centers on `document_embeddings.embedding_json`, exact in-memory scanning, and a `refresh` pipeline that does not know how to summarize before embedding. That leaves `ghcrawl` with slow clustering on large repos, no persistent ANN search surface, stale/ambiguous upgrade behavior for old embeddings and cluster runs, and no supported operator workflow for choosing summary quality versus cost. The release needs to unify those into one supported model without making users manually recover from migration state. (see origin: `docs/brainstorms/2026-04-01-vectorlite-default-search-and-summary-migration-requirements.md`) + +## Requirements Trace + +- R1. Ship one coordinated breaking release for vector search, embedding migration, and summary model controls. +- R2. Make `vectorlite` a supported runtime requirement. +- R4. Maintain a persistent vector index per repository. +- R5. Query semantic search from the persistent vector index rather than exact in-memory scans. +- R6. Support fast nearest-neighbor and likely-cluster lookups for newly synced threads. +- R8. Support `gpt-5-mini` and `gpt-5.4-mini` as summary model choices. +- R9. Default summary model to `gpt-5-mini`. +- R10. Preserve skip-on-unchanged summarization behavior with tests. +- R11. Move embeddings to `text-embedding-3-large` with explicit `dimensions=1024`. +- R12. Store one active embedding per thread for the active basis. +- R13. Support operator-selectable embedding basis: title + original description or title + summarized description. +- R14. Default embedding basis to title + summarized description. +- R15. Record pipeline metadata strongly enough to detect stale summaries, vectors, and clusters deterministically. +- R17. Force pre-release embeddings to rebuild on first `refresh` after upgrade. +- R18. Make `refresh` summary-aware when the active embedding basis depends on summaries. +- R19. Treat pre-migration cluster runs as stale after upgrade. +- R23. Add `configure` to show and change active operator settings. +- R26. Make `doctor` report `vectorlite` runtime readiness. +- R27. Publish operator cost estimates for ~20k thread one-time summarization. +- R30. Add tests for summary skipping after migration. +- R31. Add tests for stale detection across model/basis/pipeline changes. +- R32. Add tests proving semantic search uses the persistent vector index. +- R34. Validate packaging/install behavior with `vectorlite` as a hard dependency. + +## Scope Boundaries + +- Not in scope: keeping the old exact in-memory vector path as a co-equal supported production backend. +- Not in scope: a web UI redesign or any browser surface changes. +- Not in scope: preserving old cluster runs as “current” after a repo is known to need vector migration. +- Not in scope: multi-release rollout staging for vector search, embedding migration, and summary controls. + +## Context & Research + +### Relevant Code and Patterns + +- `packages/api-core/src/service.ts` + - current `refreshRepository()` only runs `sync -> embed -> cluster` + - current `summarizeRepository()` already skips unchanged work using `document_summaries.content_hash` + - current `clusterExperiment()` contains the best existing `vectorlite` integration pattern, including extension loading and HNSW queries + - current `searchRepository()` still builds semantic results from exact local embeddings rather than a persistent vector index +- `packages/api-core/src/openai/provider.ts` + - already centralizes summary and embedding API calls + - embeddings currently do not pass an explicit `dimensions` parameter +- `packages/api-core/src/config.ts` + - already persists `summaryModel` and `embedModel` + - does not yet expose embedding basis, vector backend state, or a first-class configuration workflow +- `packages/api-core/src/db/migrate.ts` + - currently persists `document_summaries`, `document_embeddings`, cluster tables, and run tables + - has no first-class pipeline-state table for migration invalidation or vector store metadata +- `apps/cli/src/main.ts` + - already has stable command/help patterns for `doctor`, `refresh`, `search`, `neighbors`, and `cluster` + - currently treats `summarize` as a dev-only command and has no `configure` command +- `packages/api-contract/src/contracts.ts` + - already defines the response contracts for `doctor`, `search`, `neighbors`, and refresh results + +### Institutional Learnings + +- `docs/solutions/performance-issues/clustering-vectorlite-hnsw-embedding-optimization-2026-03-30.md` + - recent internal evaluation found the strongest clustering quality from summary-oriented embeddings rather than raw title/body-only embeddings + - `vectorlite` HNSW materially improved clustering performance on larger datasets + - the repo’s observed summary costs support the release’s one-time migration cost messaging + +### External References + +- `vectorlite` README: persistent HNSW indexes can be backed by `index_file_path`, virtual tables support insert/update/delete by `rowid`, and metadata filtering is supported. + - +- OpenAI `text-embedding-3-large` model docs confirm the current model and pricing surface for embeddings. + - +- OpenAI `gpt-5-mini` and `gpt-5.4-mini` model docs confirm current per-token pricing and positioning. + - + - + +## Key Technical Decisions + +- **Persistent vector storage will use a sidecar repository-scoped SQLite/vectorlite store rather than embedding vectorlite tables into the canonical main DB.** + - Rationale: this keeps the issue/PR database as ordinary SQLite for easier rollback, inspection, and future recovery while still giving the product a durable ANN index. It also lets the vector store be rebuilt or replaced without entangling core relational data. + +- **The release will introduce one active vector per thread keyed by `thread_id`, with vector bytes living in the sidecar vector store and pipeline metadata living in the main DB.** + - Rationale: the current `document_embeddings` multi-row-per-thread model was useful for experimentation, but it is the wrong shape for “always-on” ANN search and creates unnecessary migration ambiguity. + +- **`refresh` will become a four-stage pipeline in behavior: `sync -> summarize-if-needed -> embed -> cluster`, while preserving the operator mental model that `refresh` is still the one command to run.** + - Rationale: if the active embedding basis depends on summaries, embedding can no longer be treated as independent of summarize. + +- **Staleness will be managed by explicit pipeline versioning, not by ad hoc assumptions about timestamps alone.** + - Rationale: this release changes multiple dimensions at once: summary model, summary prompt/version, embedding basis, embedding model, dimensions, and vector backend. A single pipeline compatibility stamp is easier to reason about than a loose mix of heuristics. + +- **Default embedding basis will be `title + dedupe summary`; default summary model will be `gpt-5-mini`.** + - Rationale: recent repo learnings suggest the summary-based embedding basis gives better clustering quality, while `gpt-5-mini` keeps one-time upgrade cost tolerable by default. + +- **`configure` will support both read and write behavior.** + - Rationale: `ghcrawl configure` with no mutation flags should show current settings and migration state; explicit flags should update persisted operator choices. This keeps the command useful for both humans and scripts. + +## Open Questions + +### Resolved During Planning + +- **Where should the persistent vector index live?** + - Resolution: in a repository-scoped sidecar vector store managed by `ghcrawl`, with the main DB retaining only relational metadata and pipeline state. + +- **Should `configure` be interactive-only or scriptable?** + - Resolution: support both. No-flag invocation shows current config and state; explicit flags mutate persisted config. + +- **Should `refresh` remain unaware of summarize?** + - Resolution: no. Summary-aware refresh is required whenever the active embedding basis depends on summaries. + +### Deferred to Implementation + +- **Exact sidecar path naming** + - Decide the final on-disk naming convention during implementation, but keep it repo-scoped and colocated with existing runtime data. + +- **How aggressively to scrub legacy `document_embeddings` rows** + - The release should stop treating them as authoritative. Whether old rows are deleted immediately or left as inert historical data for one release can be finalized during implementation. + +- **Whether to add a distinct “vector migration pending” status to TUI headers** + - This is a UX refinement worth deciding while touching the TUI status code, not a blocker to planning. + +## High-Level Technical Design + +> *This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.* + +```mermaid +flowchart TB + A["Main DB: threads / documents / document_summaries"] --> B["Pipeline state tables"] + A --> C["thread_vectors metadata (one row per thread)"] + C --> D["Repo-scoped vector sidecar"] + D --> E["vectorlite HNSW table (rowid = thread_id)"] + B --> F["stale detection"] + F --> G["refresh"] + G --> H["sync"] + H --> I["summarize if basis=title+summary and stale"] + I --> J["embed at 1024 dimensions"] + J --> K["upsert vector sidecar + thread_vectors metadata"] + K --> L["cluster from vectorlite neighbors"] + E --> M["semantic search / neighbors / cluster membership checks"] +``` + +## Implementation Units + +```mermaid +flowchart TB + U1["Unit 1: Config and migration state"] --> U2["Unit 2: Persistent vector store"] + U1 --> U3["Unit 3: Summary/vector pipeline migration"] + U2 --> U4["Unit 4: Search and clustering on vectorlite"] + U3 --> U4 + U3 --> U5["Unit 5: Refresh and operator UX"] + U4 --> U5 + U5 --> U6["Unit 6: Docs and release validation"] +``` + +- [ ] **Unit 1: Add pipeline config and repo migration state** + +**Goal:** Introduce durable configuration and per-repo state strong enough to drive a controlled migration instead of implicit behavior. + +**Requirements:** R8, R9, R13, R15, R17, R19, R23, R26, R31 + +**Dependencies:** None + +**Files:** +- Modify: `packages/api-core/src/config.ts` +- Modify: `packages/api-core/src/db/migrate.ts` +- Modify: `packages/api-core/src/service.ts` +- Modify: `packages/api-core/src/index.ts` +- Test: `packages/api-core/src/config.test.ts` +- Test: `packages/api-core/src/db/migrate.test.ts` +- Test: `packages/api-core/src/service.test.ts` + +**Approach:** +- Extend persisted config with: + - summary model selection + - embedding basis selection + - any explicit vector backend / vector store config needed for this release +- Add repo-scoped pipeline state in the main DB to record: + - current migration compatibility version + - active summary model / prompt version + - active embedding basis / embedding model / dimensions + - vector index freshness + - cluster freshness relative to vectors +- Make stale-state computation explicit in service helpers rather than distributing it across command handlers. + +**Execution note:** Start with migration-state and config tests before touching orchestration logic so later units have a stable contract to build on. + +**Patterns to follow:** +- `packages/api-core/src/config.ts` persisted config loading/writing +- `packages/api-core/src/db/migrate.ts` additive schema migration style +- `packages/api-core/src/service.ts` repo-scoped run and state helpers + +**Test scenarios:** +- Happy path: loading config with no new settings defaults to `gpt-5-mini` and the default embedding basis. +- Happy path: persisted config round-trips the chosen summary model and embedding basis. +- Edge case: a repo with no pipeline state is treated as migration-pending rather than current. +- Edge case: changing summary model or embedding basis marks repo vector/cluster state stale. +- Error path: malformed persisted config values are ignored or rejected consistently with current config behavior. +- Integration: startup migration on an existing pre-release DB adds the new pipeline state tables without destroying existing threads, summaries, or run history. + +**Verification:** +- The config object exposes the new settings predictably. +- A migrated DB can answer “is this repo current, stale, or migration-pending?” deterministically. + +- [ ] **Unit 2: Introduce a persistent vectorlite sidecar store** + +**Goal:** Replace ephemeral experiment-only vectorlite usage with a supported persistent vector store abstraction. + +**Requirements:** R2, R4, R5, R6, R7, R12, R15, R32, R34 + +**Dependencies:** Unit 1 + +**Files:** +- Create: `packages/api-core/src/vector/store.ts` +- Create: `packages/api-core/src/vector/vectorlite-store.ts` +- Modify: `packages/api-core/src/db/sqlite.ts` +- Modify: `packages/api-core/src/service.ts` +- Modify: `packages/api-core/src/index.ts` +- Test: `packages/api-core/src/vector/vectorlite-store.test.ts` +- Test: `packages/api-core/src/service.test.ts` + +**Approach:** +- Introduce a dedicated vector-store abstraction that owns: + - extension loading + - sidecar file lifecycle + - persistent HNSW virtual table creation + - upsert/delete/query by `thread_id` +- Use a single active vector table per repo with `rowid = thread_id`. +- Keep vector values in the sidecar store rather than in the main DB as JSON. +- Keep enough metadata in the main DB to detect whether a thread’s stored vector is current without needing to inspect vector payloads. +- Make vector store open/health behavior explicit so `doctor` and runtime commands can report extension problems cleanly. + +**Execution note:** Build this behind a narrow interface first; do not spread raw vectorlite SQL across service methods. + +**Patterns to follow:** +- `packages/api-core/src/service.ts` current `clusterExperiment()` vectorlite extension-loading pattern +- `packages/api-core/src/db/sqlite.ts` SQLite open/pragma helpers + +**Test scenarios:** +- Happy path: a vector store can create/open a repo sidecar and persist vectors across connection reopen. +- Happy path: querying nearest neighbors returns the expected thread ids after reopen, without rebuilding the index. +- Edge case: deleting a thread vector removes it from later ANN results. +- Edge case: updating an existing thread vector replaces the old vector rather than duplicating it. +- Error path: missing vectorlite extension surfaces an actionable error that `doctor` can report. +- Integration: service helpers can upsert/query/delete vectors through the abstraction without touching temp experiment paths. + +**Verification:** +- A migrated repo can reopen its vector store and serve neighbor queries without loading the entire corpus into process memory. + +- [ ] **Unit 3: Migrate summaries and embeddings to one active vector per thread** + +**Goal:** Rebuild the content pipeline around one active vector per thread, explicit 1024-dimensional embeddings, and summary-aware stale detection. + +**Requirements:** R10, R11, R12, R13, R14, R15, R16, R17, R18, R30, R31 + +**Dependencies:** Unit 1, Unit 2 + +**Files:** +- Modify: `packages/api-core/src/openai/provider.ts` +- Modify: `packages/api-core/src/service.ts` +- Modify: `packages/api-core/src/db/migrate.ts` +- Modify: `packages/api-core/src/search/exact.ts` +- Test: `packages/api-core/src/service.test.ts` +- Test: `packages/api-core/src/openai/provider.test.ts` (or add a new targeted provider/unit test file if one does not exist yet) + +**Approach:** +- Add explicit embedding dimensions support to the provider and pass `dimensions=1024` for `text-embedding-3-large`. +- Replace the current multi-source embedding workset with one active vector input per thread derived from the configured basis. +- Keep summary generation as a prerequisite only when the chosen basis requires it. +- Extend summary stale detection so it remains keyed by content hash and summary model, and add any prompt/pipeline version metadata needed to invalidate old summaries intentionally. +- Add vector metadata keyed by thread id so the system can decide whether a thread needs re-embedding without reading the sidecar vector value. +- Ensure upgraded repos treat legacy vectors as stale and rebuild them on the next refresh. + +**Execution note:** Characterization-first around summary skipping and embedding workset selection will reduce migration regressions here. + +**Patterns to follow:** +- `packages/api-core/src/service.ts` current `summarizeRepository()` skip-on-content-hash behavior +- `packages/api-core/src/service.ts` current `embedRepository()` batching/progress patterns +- `packages/api-core/src/openai/provider.ts` retry behavior and model call centralization + +**Test scenarios:** +- Happy path: unchanged summary input under the same summary model is skipped on repeated runs. +- Happy path: basis=`title+original` produces vectors without requiring summaries. +- Happy path: basis=`title+summary` triggers summarize first when dedupe summaries are missing or stale. +- Edge case: changing summary model marks summary-derived vectors stale. +- Edge case: changing embedding basis marks vectors stale even when summaries remain current. +- Edge case: pre-release `document_embeddings` rows are ignored as current vectors after migration. +- Error path: embedding provider failures do not falsely mark vectors current. +- Integration: a repo upgraded from the old schema rebuilds vectors to 1024 dimensions on first eligible refresh. + +**Verification:** +- After migration, each active thread has exactly one current vector metadata row and one current sidecar vector entry for the chosen basis. + +- [ ] **Unit 4: Move semantic search, neighbors, and cluster building onto vectorlite** + +**Goal:** Make persistent vectorlite the supported engine for semantic lookup and cluster construction. + +**Requirements:** R3, R4, R5, R6, R19, R22, R32 + +**Dependencies:** Unit 2, Unit 3 + +**Files:** +- Modify: `packages/api-core/src/service.ts` +- Modify: `packages/api-core/src/api/server.ts` +- Modify: `packages/api-core/src/cluster/build.ts` +- Modify: `packages/api-contract/src/contracts.ts` +- Test: `packages/api-core/src/service.test.ts` +- Test: `packages/api-core/src/api/server.test.ts` +- Test: `packages/api-core/src/cluster/perf.integration.ts` + +**Approach:** +- Replace semantic search candidate generation with ANN neighbor lookup from the persistent vector store. +- Replace `neighbors` lookup with vector store queries rather than exact local scans. +- Rebuild cluster edge generation from vector store neighbor queries instead of loading the full embedding corpus into RAM. +- Make service methods return clear “migration pending / vectors stale / clusters stale” responses when the repo has not completed the required rebuild yet. +- Keep cluster persistence and local close-state behavior aligned with the current cluster run model unless the implementation uncovers a simpler equivalent. + +**Execution note:** Start with search/neighbors characterization coverage so the ANN transition preserves the public contract while changing the engine underneath. + +**Patterns to follow:** +- `packages/api-core/src/service.ts` existing `searchRepository()`, `listNeighbors()`, and cluster persistence flow +- `packages/api-contract/src/contracts.ts` current JSON contract style +- `packages/api-core/src/cluster/perf.integration.ts` current perf comparison harness + +**Test scenarios:** +- Happy path: semantic search on a migrated repo returns ANN-backed hits without loading exact embeddings. +- Happy path: neighbors for a migrated thread come from the persistent vector store. +- Happy path: cluster rebuild on a migrated repo uses vectorlite and persists clusters successfully. +- Edge case: repo marked migration-pending returns a clear stale-state error or status rather than misleading old clusters. +- Edge case: closed/deleted threads are excluded from later neighbor and cluster results after vector-store cleanup. +- Integration: API server endpoints for search and neighbors continue returning valid contracts after the backend swap. +- Integration: perf harness can compare exact legacy-style behavior versus persistent vectorlite behavior on synthetic fixtures. + +**Verification:** +- Semantic queries and cluster builds no longer require loading the full embedding corpus from JSON rows into process memory. + +- [ ] **Unit 5: Redefine refresh and add operator configuration/health UX** + +**Goal:** Make the operator experience understandable and safe during and after the migration. + +**Requirements:** R17, R18, R19, R20, R21, R22, R23, R24, R25, R26, R29 + +**Dependencies:** Unit 1, Unit 3, Unit 4 + +**Files:** +- Modify: `apps/cli/src/main.ts` +- Modify: `apps/cli/src/main.test.ts` +- Modify: `apps/cli/src/init-wizard.ts` +- Modify: `apps/cli/src/tui/app.ts` +- Modify: `packages/api-core/src/service.ts` +- Modify: `packages/api-contract/src/contracts.ts` +- Test: `apps/cli/src/main.test.ts` +- Test: `packages/api-core/src/service.test.ts` + +**Approach:** +- Add `ghcrawl configure`: + - no flags: show current summary model, embedding basis, vector readiness, and migration state + - explicit flags: change summary model and embedding basis intentionally +- Extend `doctor` to report `vectorlite` loadability and any migration readiness signals worth surfacing globally. +- Redefine `refresh` so it can automatically run summarize when the active basis requires it, while preserving the one-command operator experience. +- Update TUI status text where needed so stale vectors/clusters are visible and not mistaken for current data. +- Preserve existing spend progress messaging and expand it where the first post-upgrade refresh may trigger a large summary rebuild. + +**Patterns to follow:** +- `apps/cli/src/main.ts` current command/help formatting and doctor output patterns +- `apps/cli/src/init-wizard.ts` current config write and operator guidance patterns +- `apps/cli/src/tui/app.ts` current repo status/header conventions + +**Test scenarios:** +- Happy path: `configure` with no flags reports the active summary model, embedding basis, and migration status. +- Happy path: `configure` updates the summary model and persists it. +- Happy path: refresh on a migration-pending repo runs summarize before embed when basis=`title+summary`. +- Edge case: refresh on basis=`title+original` skips summarize unless the user explicitly requests summary work. +- Edge case: doctor reports vectorlite readiness failures cleanly when the extension cannot load. +- Error path: user switches summary model and is told the repo now needs rebuild work rather than being shown stale current state. +- Integration: TUI and CLI both surface repo migration/stale status consistently after settings changes. + +**Verification:** +- Operators can discover current settings and complete the first migration refresh without consulting source code or guessing which hidden command to run. + +- [ ] **Unit 6: Update docs, release notes, and upgrade validation** + +**Goal:** Make the release understandable to operators and safe to ship. + +**Requirements:** R1, R27, R28, R29, R33, R34 + +**Dependencies:** Units 1-5 + +**Files:** +- Modify: `README.md` +- Modify: `apps/cli/README.md` +- Modify: `CONTRIBUTING.md` +- Modify: `docs/DESIGN.md` +- Modify: `docs/PLAN.md` +- Modify: `.github/workflows/ci.yml` +- Modify: `.github/workflows/publish.yml` (if packaging validation needs to change) +- Test: `packages/api-core/src/cluster/perf.integration.ts` +- Test expectation: none -- docs files themselves do not need behavioral tests, but the release validation steps must be represented in automated checks where practical + +**Approach:** +- Update operator docs to explain: + - `vectorlite` is now required + - first refresh after upgrade may trigger a one-time rebuild + - `gpt-5-mini` vs `gpt-5.4-mini` tradeoffs and costs + - semantic search now depends on the persistent vector index +- Publish a concrete upgrade story for pre-vectorlite users. +- Extend CI/package validation enough to catch missing `vectorlite` packaging or install regressions on supported platforms. +- Preserve or refine the existing cluster perf workflow so future PRs can still compare the vector path meaningfully. + +**Patterns to follow:** +- current README operator sections +- current CI/package smoke workflows +- current cluster perf workflow/comment structure + +**Test scenarios:** +- Happy path: package smoke or install validation confirms `vectorlite` is present and the CLI still launches on supported platforms. +- Edge case: upgrade validation from a pre-vectorlite DB shows first refresh drives the required rebuild path without manual DB surgery. +- Integration: CI continues to publish meaningful cluster perf comparisons after the migration lands. + +**Verification:** +- A maintainer can follow the docs to understand migration cost, migration steps, and expected behavior on first refresh. + +## System-Wide Impact + +- **Interaction graph:** `sync`, `summarize`, `embed`, `search`, `neighbors`, `cluster`, `refresh`, `doctor`, `configure`, TUI status surfaces, and package/install flows all participate in this release. +- **Error propagation:** vector store open/load failures must surface as health/configuration errors, not as silent empty search results. +- **State lifecycle risks:** the main DB and vector sidecar must stay logically synchronized when threads close, content changes, configs change, or refresh is interrupted mid-run. +- **API surface parity:** CLI and local HTTP API should report migration state consistently for search- and cluster-dependent commands. +- **Integration coverage:** upgrade-from-old-DB scenarios, vector store reopen scenarios, and refresh-after-setting-change scenarios need integration coverage beyond isolated unit tests. +- **Unchanged invariants:** GitHub sync remains the source of truth for thread metadata and local close-state behavior remains intact; this plan changes vector/search infrastructure, not the core sync contract. + +## Risks & Dependencies + +| Risk | Mitigation | +|------|------------| +| `vectorlite` native packaging breaks on one or more supported platforms | Add package smoke and install validation, surface `doctor` readiness clearly, and keep sidecar/vector-store code isolated so failures are diagnosable | +| Old cluster runs appear current after upgrade | Add explicit repo pipeline state and invalidate cluster freshness when pipeline compatibility changes | +| Summary-derived embedding migration silently re-summarizes too much and causes unnecessary spend | Preserve content-hash-based skipping, add model/basis-aware stale detection, and keep spend progress reporting visible during summarize/refresh | +| Main DB and vector sidecar drift out of sync | Centralize vector upsert/delete through one abstraction and update pipeline state only after successful writes | +| Search quality regresses if vectorlite and summary-basis defaults are not aligned with current learnings | Use the recent internal clustering solution doc as the default rationale and keep perf/quality comparison harnesses intact | + +## Documentation / Operational Notes + +- Document the one-time cost estimate for ~20k-thread full summarization with both summary models. +- Document that the first post-upgrade refresh may take meaningfully longer than future refreshes because it performs migration work. +- Document that semantic search and nearest-neighbor features are only trustworthy after repo migration completes. +- Include a short maintainer release note calling out the breaking nature of the vector/search migration. + +## Sources & References + +- **Origin document:** `docs/brainstorms/2026-04-01-vectorlite-default-search-and-summary-migration-requirements.md` +- Related code: + - `packages/api-core/src/service.ts` + - `packages/api-core/src/openai/provider.ts` + - `packages/api-core/src/config.ts` + - `packages/api-core/src/db/migrate.ts` + - `packages/api-contract/src/contracts.ts` + - `apps/cli/src/main.ts` +- Institutional learning: + - `docs/solutions/performance-issues/clustering-vectorlite-hnsw-embedding-optimization-2026-03-30.md` +- External docs: + - + - + - + - From 2c0e2a800f6e0e94600b53458a47c1591859d854 Mon Sep 17 00:00:00 2001 From: huntharo Date: Wed, 1 Apr 2026 19:52:24 -0400 Subject: [PATCH 26/34] feat: migrate core pipeline to persistent vectorlite vectors --- packages/api-core/src/api/server.test.ts | 14 + .../api-core/src/cluster/perf.integration.ts | 2 + packages/api-core/src/config.test.ts | 27 + packages/api-core/src/config.ts | 31 + packages/api-core/src/db/migrate.test.ts | 5 + packages/api-core/src/db/migrate.ts | 43 ++ packages/api-core/src/index.ts | 2 + packages/api-core/src/openai/provider.ts | 5 +- packages/api-core/src/service.test.ts | 66 +- packages/api-core/src/service.ts | 716 +++++++++++++++--- packages/api-core/src/vector/store.ts | 28 + .../src/vector/vectorlite-store.test.ts | 88 +++ .../api-core/src/vector/vectorlite-store.ts | 159 ++++ 13 files changed, 1043 insertions(+), 143 deletions(-) create mode 100644 packages/api-core/src/vector/store.ts create mode 100644 packages/api-core/src/vector/vectorlite-store.test.ts create mode 100644 packages/api-core/src/vector/vectorlite-store.ts diff --git a/packages/api-core/src/api/server.test.ts b/packages/api-core/src/api/server.test.ts index 0661f04..869e189 100644 --- a/packages/api-core/src/api/server.test.ts +++ b/packages/api-core/src/api/server.test.ts @@ -21,6 +21,8 @@ test('health endpoint returns contract payload', async () => { openaiApiKeySource: 'none', summaryModel: 'gpt-5-mini', embedModel: 'text-embedding-3-large', + embeddingBasis: 'title_original', + vectorBackend: 'vectorlite', embedBatchSize: 8, embedConcurrency: 10, embedMaxUnread: 20, @@ -71,6 +73,8 @@ test('neighbors endpoint returns contract payload', async () => { openaiApiKeySource: 'none', summaryModel: 'gpt-5-mini', embedModel: 'text-embedding-3-large', + embeddingBasis: 'title_original', + vectorBackend: 'vectorlite', embedBatchSize: 8, embedConcurrency: 10, embedMaxUnread: 20, @@ -163,6 +167,8 @@ test('threads endpoint can filter by a bulk number list', async () => { openaiApiKeySource: 'none', summaryModel: 'gpt-5-mini', embedModel: 'text-embedding-3-large', + embeddingBasis: 'title_original', + vectorBackend: 'vectorlite', embedBatchSize: 8, embedConcurrency: 10, embedMaxUnread: 20, @@ -232,6 +238,8 @@ test('author-threads endpoint returns one author with strongest same-author matc openaiApiKeySource: 'none', summaryModel: 'gpt-5-mini', embedModel: 'text-embedding-3-large', + embeddingBasis: 'title_original', + vectorBackend: 'vectorlite', embedBatchSize: 8, embedConcurrency: 10, embedMaxUnread: 20, @@ -311,6 +319,8 @@ test('close-thread and includeClosed thread routes expose locally closed items', openaiApiKeySource: 'none', summaryModel: 'gpt-5-mini', embedModel: 'text-embedding-3-large', + embeddingBasis: 'title_original', + vectorBackend: 'vectorlite', embedBatchSize: 8, embedConcurrency: 10, embedMaxUnread: 20, @@ -394,6 +404,8 @@ test('server returns 400 for malformed request inputs', async () => { openaiApiKeySource: 'none', summaryModel: 'gpt-5-mini', embedModel: 'text-embedding-3-large', + embeddingBasis: 'title_original', + vectorBackend: 'vectorlite', embedBatchSize: 8, embedConcurrency: 10, embedMaxUnread: 20, @@ -448,6 +460,8 @@ test('cluster summary and detail endpoints return contract payloads', async () = openaiApiKeySource: 'none', summaryModel: 'gpt-5-mini', embedModel: 'text-embedding-3-large', + embeddingBasis: 'title_original', + vectorBackend: 'vectorlite', embedBatchSize: 8, embedConcurrency: 10, embedMaxUnread: 20, diff --git a/packages/api-core/src/cluster/perf.integration.ts b/packages/api-core/src/cluster/perf.integration.ts index f738d36..bce518f 100644 --- a/packages/api-core/src/cluster/perf.integration.ts +++ b/packages/api-core/src/cluster/perf.integration.ts @@ -201,6 +201,8 @@ function createService(dbPath: string): GHCrawlService { openaiApiKeySource: 'none', summaryModel: 'gpt-5-mini', embedModel: 'text-embedding-3-large', + embeddingBasis: 'title_original', + vectorBackend: 'vectorlite', embedBatchSize: 2, embedConcurrency: 2, embedMaxUnread: 4, diff --git a/packages/api-core/src/config.test.ts b/packages/api-core/src/config.test.ts index 3b19483..ce68be7 100644 --- a/packages/api-core/src/config.test.ts +++ b/packages/api-core/src/config.test.ts @@ -55,6 +55,9 @@ test('loadConfig prefers persisted config and stores defaults under the user con assert.equal(config.githubTokenSource, 'config'); assert.equal(config.openaiApiKeySource, 'config'); assert.equal(config.dbPath, path.join(home, '.config', 'ghcrawl', 'ghcrawl.db')); + assert.equal(config.summaryModel, 'gpt-5-mini'); + assert.equal(config.embeddingBasis, 'title_summary'); + assert.equal(config.vectorBackend, 'vectorlite'); }); test('loadConfig lets environment variables override persisted config', () => { @@ -163,6 +166,30 @@ test('writePersistedConfig creates a readable config file', () => { assert.equal(persisted.data.openaiApiKey, 'sk-proj-testkey1234567890'); }); +test('persisted config round-trips summary model, embedding basis, and vector backend', () => { + const home = makeTempHome(); + const env = { + ...makeTestEnv(), + HOME: home, + }; + + writePersistedConfig( + { + githubToken: 'ghp_testtoken1234567890', + openaiApiKey: 'sk-proj-testkey1234567890', + summaryModel: 'gpt-5.4-mini', + embeddingBasis: 'title_original', + vectorBackend: 'vectorlite', + }, + { env }, + ); + + const loaded = loadConfig({ env, cwd: process.cwd() }); + assert.equal(loaded.summaryModel, 'gpt-5.4-mini'); + assert.equal(loaded.embeddingBasis, 'title_original'); + assert.equal(loaded.vectorBackend, 'vectorlite'); +}); + test('config path override redirects persisted config reads and writes', () => { const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'ghcrawl-workspace-')); const overridePath = path.join(workspace, '.tmp-config', 'custom-config.json'); diff --git a/packages/api-core/src/config.ts b/packages/api-core/src/config.ts index a792dd7..e85bdea 100644 --- a/packages/api-core/src/config.ts +++ b/packages/api-core/src/config.ts @@ -9,6 +9,8 @@ export type SecretProvider = 'plaintext' | 'op'; export type TuiSortPreference = 'recent' | 'size'; export type TuiMinClusterSize = 0 | 1 | 10 | 20 | 50; export type TuiWideLayoutPreference = 'columns' | 'right-stack'; +export type EmbeddingBasis = 'title_original' | 'title_summary'; +export type VectorBackend = 'vectorlite'; export type TuiRepositoryPreference = { minClusterSize: TuiMinClusterSize; @@ -26,6 +28,8 @@ export type PersistedGitcrawlConfig = { apiPort?: number; summaryModel?: string; embedModel?: string; + embeddingBasis?: EmbeddingBasis; + vectorBackend?: VectorBackend; embedBatchSize?: number; embedConcurrency?: number; embedMaxUnread?: number; @@ -51,6 +55,8 @@ export type GitcrawlConfig = { opItemName?: string; summaryModel: string; embedModel: string; + embeddingBasis: EmbeddingBasis; + vectorBackend: VectorBackend; embedBatchSize: number; embedConcurrency: number; embedMaxUnread: number; @@ -174,6 +180,14 @@ function getTuiWideLayoutPreference(value: unknown): TuiWideLayoutPreference | u return value === 'columns' || value === 'right-stack' ? value : undefined; } +function getEmbeddingBasis(value: unknown): EmbeddingBasis | undefined { + return value === 'title_original' || value === 'title_summary' ? value : undefined; +} + +function getVectorBackend(value: unknown): VectorBackend | undefined { + return value === 'vectorlite' ? value : undefined; +} + function getTuiPreferences(value: unknown): Record | undefined { if (!value || typeof value !== 'object') { return undefined; @@ -219,6 +233,8 @@ export function readPersistedConfig(options: LoadConfigOptions = {}): LoadedStor apiPort: getNumber(raw.apiPort), summaryModel: getString(raw.summaryModel), embedModel: getString(raw.embedModel), + embeddingBasis: getEmbeddingBasis(raw.embeddingBasis), + vectorBackend: getVectorBackend(raw.vectorBackend), embedBatchSize: getNumber(raw.embedBatchSize), embedConcurrency: getNumber(raw.embedConcurrency), embedMaxUnread: getNumber(raw.embedMaxUnread), @@ -337,6 +353,18 @@ export function loadConfig(options: LoadConfigOptions = {}): GitcrawlConfig { { source: 'dotenv', value: getDotenvString(dotenvValues, 'GHCRAWL_EMBED_MODEL', 'GHCRAWL_EMBED_MODEL') }, { source: 'default', value: 'text-embedding-3-large' }, ); + const embeddingBasis = pickDefined( + { source: 'env', value: getEmbeddingBasis(getEnvString(env, 'GHCRAWL_EMBEDDING_BASIS', 'GHCRAWL_EMBEDDING_BASIS')) }, + { source: 'config', value: stored.data.embeddingBasis }, + { source: 'dotenv', value: getEmbeddingBasis(getDotenvString(dotenvValues, 'GHCRAWL_EMBEDDING_BASIS', 'GHCRAWL_EMBEDDING_BASIS')) }, + { source: 'default', value: 'title_summary' }, + ); + const vectorBackend = pickDefined( + { source: 'env', value: getVectorBackend(getEnvString(env, 'GHCRAWL_VECTOR_BACKEND', 'GHCRAWL_VECTOR_BACKEND')) }, + { source: 'config', value: stored.data.vectorBackend }, + { source: 'dotenv', value: getVectorBackend(getDotenvString(dotenvValues, 'GHCRAWL_VECTOR_BACKEND', 'GHCRAWL_VECTOR_BACKEND')) }, + { source: 'default', value: 'vectorlite' }, + ); const openSearchUrl = pickDefined( { source: 'env', value: getEnvString(env, 'GHCRAWL_OPENSEARCH_URL', 'GHCRAWL_OPENSEARCH_URL') }, { source: 'config', value: stored.data.openSearchUrl }, @@ -375,6 +403,8 @@ export function loadConfig(options: LoadConfigOptions = {}): GitcrawlConfig { opItemName: stored.data.opItemName, summaryModel: summaryModel.value ?? 'gpt-5-mini', embedModel: embedModel.value ?? 'text-embedding-3-large', + embeddingBasis: embeddingBasis.value ?? 'title_summary', + vectorBackend: vectorBackend.value ?? 'vectorlite', embedBatchSize, embedConcurrency, embedMaxUnread, @@ -387,6 +417,7 @@ export function loadConfig(options: LoadConfigOptions = {}): GitcrawlConfig { export function ensureRuntimeDirs(config: GitcrawlConfig): void { fs.mkdirSync(config.configDir, { recursive: true }); fs.mkdirSync(path.dirname(config.dbPath), { recursive: true }); + fs.mkdirSync(path.join(config.configDir, 'vectors'), { recursive: true }); } export function getTuiRepositoryPreference(config: GitcrawlConfig, owner: string, repo: string): TuiRepositoryPreference { diff --git a/packages/api-core/src/db/migrate.test.ts b/packages/api-core/src/db/migrate.test.ts index 7aba573..476cc5a 100644 --- a/packages/api-core/src/db/migrate.test.ts +++ b/packages/api-core/src/db/migrate.test.ts @@ -17,13 +17,18 @@ test('migrate creates core tables', () => { assert.ok(names.includes('threads')); assert.ok(names.includes('documents')); assert.ok(names.includes('document_embeddings')); + assert.ok(names.includes('thread_vectors')); assert.ok(names.includes('cluster_runs')); assert.ok(names.includes('repo_sync_state')); + assert.ok(names.includes('repo_pipeline_state')); const threadColumns = db.prepare('pragma table_info(threads)').all() as Array<{ name: string }>; const threadColumnNames = threadColumns.map((column) => column.name); assert.ok(threadColumnNames.includes('first_pulled_at')); assert.ok(threadColumnNames.includes('last_pulled_at')); + + const summaryColumns = db.prepare('pragma table_info(document_summaries)').all() as Array<{ name: string }>; + assert.ok(summaryColumns.map((column) => column.name).includes('prompt_version')); } finally { db.close(); } diff --git a/packages/api-core/src/db/migrate.ts b/packages/api-core/src/db/migrate.ts index 7ec4059..36c4e3e 100644 --- a/packages/api-core/src/db/migrate.ts +++ b/packages/api-core/src/db/migrate.ts @@ -125,6 +125,34 @@ const migrationStatements = [ ) `, ` + create table if not exists thread_vectors ( + thread_id integer primary key references threads(id) on delete cascade, + basis text not null, + model text not null, + dimensions integer not null, + content_hash text not null, + vector_json text not null, + vector_backend text not null, + created_at text not null, + updated_at text not null + ) + `, + ` + create table if not exists repo_pipeline_state ( + repo_id integer primary key references repositories(id) on delete cascade, + summary_model text not null, + summary_prompt_version text not null, + embedding_basis text not null, + embed_model text not null, + embed_dimensions integer not null, + embed_pipeline_version text not null, + vector_backend text not null, + vectors_current_at text, + clusters_current_at text, + updated_at text not null + ) + `, + ` create table if not exists sync_runs ( id integer primary key, repo_id integer references repositories(id) on delete cascade, @@ -249,8 +277,23 @@ export function migrate(db: SqliteDatabase): void { db.exec('alter table clusters add column close_reason_local text'); } + const summaryColumns = new Set( + (db.prepare('pragma table_info(document_summaries)').all() as Array<{ name: string }>).map((column) => column.name), + ); + if (!summaryColumns.has('prompt_version')) { + db.exec("alter table document_summaries add column prompt_version text default 'v1'"); + } + + const vectorColumns = new Set( + (db.prepare('pragma table_info(thread_vectors)').all() as Array<{ name: string }>).map((column) => column.name), + ); + if (!vectorColumns.has('vector_backend')) { + db.exec("alter table thread_vectors add column vector_backend text default 'vectorlite'"); + } + db.exec('create index if not exists idx_threads_repo_number on threads(repo_id, number)'); db.exec('create index if not exists idx_document_summaries_thread_model on document_summaries(thread_id, model)'); + db.exec('create index if not exists idx_thread_vectors_basis_model on thread_vectors(basis, model)'); db.exec('create index if not exists idx_cluster_runs_repo_status_id on cluster_runs(repo_id, status, id)'); db.exec('create index if not exists idx_clusters_repo_run_id on clusters(repo_id, cluster_run_id, id)'); db.exec('create index if not exists idx_cluster_members_thread_cluster on cluster_members(thread_id, cluster_id)'); diff --git a/packages/api-core/src/index.ts b/packages/api-core/src/index.ts index 15471e0..0119310 100644 --- a/packages/api-core/src/index.ts +++ b/packages/api-core/src/index.ts @@ -4,3 +4,5 @@ export * from './documents/normalize.js'; export * from './search/exact.js'; export * from './cluster/build.js'; export * from './service.js'; +export * from './vector/store.js'; +export * from './vector/vectorlite-store.js'; diff --git a/packages/api-core/src/openai/provider.ts b/packages/api-core/src/openai/provider.ts index b83f02a..53a6cc3 100644 --- a/packages/api-core/src/openai/provider.ts +++ b/packages/api-core/src/openai/provider.ts @@ -21,7 +21,7 @@ export type SummaryUsage = { export type AiProvider = { checkAuth: () => Promise; summarizeThread: (params: { model: string; text: string }) => Promise<{ summary: SummaryResult; usage?: SummaryUsage }>; - embedTexts: (params: { model: string; texts: string[] }) => Promise; + embedTexts: (params: { model: string; texts: string[]; dimensions?: number }) => Promise; }; const summarySchema = z.object({ @@ -116,7 +116,7 @@ export class OpenAiProvider implements AiProvider { throw new Error(`OpenAI summarization failed after 3 attempts: ${lastError?.message ?? 'unknown error'}`); } - async embedTexts(params: { model: string; texts: string[] }): Promise { + async embedTexts(params: { model: string; texts: string[]; dimensions?: number }): Promise { if (params.texts.length === 0) { return []; } @@ -127,6 +127,7 @@ export class OpenAiProvider implements AiProvider { const response = await this.client.embeddings.create({ model: params.model, input: params.texts, + dimensions: params.dimensions, }); return response.data.map((item) => item.embedding); diff --git a/packages/api-core/src/service.test.ts b/packages/api-core/src/service.test.ts index 9d71229..f87989b 100644 --- a/packages/api-core/src/service.test.ts +++ b/packages/api-core/src/service.test.ts @@ -1,13 +1,17 @@ import test from 'node:test'; import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import { GHCrawlService } from './service.js'; function makeTestConfig(overrides: Partial = {}): GHCrawlService['config'] { + const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghcrawl-service-test-')); return { workspaceRoot: process.cwd(), - configDir: '/tmp/ghcrawl-test', - configPath: '/tmp/ghcrawl-test/config.json', + configDir, + configPath: path.join(configDir, 'config.json'), configFileExists: true, dbPath: ':memory:', dbPathSource: 'config', @@ -19,6 +23,8 @@ function makeTestConfig(overrides: Partial = {}): GHCr openaiApiKeySource: 'none', summaryModel: 'gpt-5-mini', embedModel: 'text-embedding-3-large', + embeddingBasis: 'title_original', + vectorBackend: 'vectorlite', embedBatchSize: 2, embedConcurrency: 2, embedMaxUnread: 4, @@ -38,6 +44,14 @@ function makeTestService( }); } +function makeEmbedding(seed: number, variant = 0): number[] { + return Array.from({ length: 1024 }, (_value, index) => { + if (index === 0) return seed; + if (index === 1) return variant; + return 0; + }); +} + test('doctor reports config path and successful auth smoke checks', async () => { let githubChecked = 0; let openAiChecked = 0; @@ -71,11 +85,13 @@ test('doctor reports config path and successful auth smoke checks', async () => try { const result = await service.doctor(); - assert.equal(result.health.configPath, '/tmp/ghcrawl-test/config.json'); + assert.equal(result.health.configPath, service.config.configPath); assert.equal(result.github.formatOk, true); assert.equal(result.github.authOk, true); assert.equal(result.openai.formatOk, true); assert.equal(result.openai.authOk, true); + assert.equal(result.vectorlite.configured, true); + assert.equal(result.vectorlite.runtimeOk, true); assert.equal(githubChecked, 1); assert.equal(openAiChecked, 1); } finally { @@ -703,7 +719,7 @@ test('embedRepository batches multi-source embeddings and skips unchanged inputs }, embedTexts: async ({ texts }) => { embedCalls.push(texts); - return texts.map((text, index) => [text.length, index]); + return texts.map((text, index) => makeEmbedding(text.length, index)); }, }, ); @@ -751,33 +767,33 @@ test('embedRepository batches multi-source embeddings and skips unchanged inputs ); service.db .prepare( - `insert into document_summaries (thread_id, summary_kind, model, content_hash, summary_text, created_at, updated_at) - values (?, ?, ?, ?, ?, ?, ?)`, + `insert into document_summaries (thread_id, summary_kind, model, prompt_version, content_hash, summary_text, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?)`, ) - .run(10, 'dedupe_summary', 'gpt-5-mini', 'summary-hash', 'Transfer hangs near completion.', now, now); + .run(10, 'dedupe_summary', 'gpt-5-mini', 'v1', 'summary-hash', 'Transfer hangs near completion.', now, now); const first = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); - assert.equal(first.embedded, 3); - assert.equal(embedCalls.length, 2); + assert.equal(first.embedded, 1); + assert.equal(embedCalls.length, 1); assert.deepEqual( service.db - .prepare('select source_kind from document_embeddings order by source_kind asc') + .prepare('select basis from thread_vectors order by basis asc') .all() - .map((row: unknown) => (row as { source_kind: string }).source_kind), - ['body', 'dedupe_summary', 'title'], + .map((row: unknown) => (row as { basis: string }).basis), + ['title_original'], ); const second = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); assert.equal(second.embedded, 0); - assert.equal(embedCalls.length, 2); + assert.equal(embedCalls.length, 1); service.db .prepare('update threads set body = ?, updated_at = ? where id = ?') .run('The transfer now stalls at 99%.', now, 10); const third = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); assert.equal(third.embedded, 1); - assert.equal(embedCalls.length, 3); - assert.deepEqual(embedCalls[2], ['The transfer now stalls at 99%.']); + assert.equal(embedCalls.length, 2); + assert.deepEqual(embedCalls[1], ['title: Downloader hangs\n\nbody: The transfer now stalls at 99%.']); } finally { service.close(); } @@ -812,7 +828,7 @@ test('embedRepository truncates oversized inputs before submission', async () => }, embedTexts: async ({ texts }) => { embedCalls.push(texts); - return texts.map((text, index) => [text.length, index]); + return texts.map((text, index) => makeEmbedding(text.length, index)); }, }, }); @@ -895,7 +911,7 @@ test('embedRepository truncates oversized inputs before submission', async () => const result = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); - assert.equal(result.embedded, 4); + assert.equal(result.embedded, 2); assert.ok(embedCalls.length >= 1); const truncatedBodies = embedCalls.flat().filter((text) => text.includes('[truncated for embedding]')); assert.equal(truncatedBodies.length, 2); @@ -943,7 +959,7 @@ test('embedRepository isolates a failing oversized item from a mixed batch and r ); } } - return texts.map((text, index) => [text.length, index]); + return texts.map((text, index) => makeEmbedding(text.length, index)); }, }, }); @@ -1025,9 +1041,9 @@ test('embedRepository isolates a failing oversized item from a mixed batch and r const result = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); - assert.equal(result.embedded, 4); + assert.equal(result.embedded, 2); assert.ok(embedCalls.length >= 3); - assert.equal(embedCalls[0].length, 4); + assert.equal(embedCalls[0].length, 2); assert.ok(embedCalls.flat().some((text) => text.includes('[truncated for embedding]'))); } finally { service.close(); @@ -1069,7 +1085,7 @@ test('embedRepository recovers from wrapped maximum input length errors by shrin `OpenAI embeddings failed after 5 attempts: 400 Invalid 'input[${overLimitIndex}]': maximum input length is 8192 tokens.`, ); } - return texts.map((text, index) => [text.length, index]); + return texts.map((text, index) => makeEmbedding(text.length, index)); }, }, }); @@ -1151,7 +1167,7 @@ test('embedRepository recovers from wrapped maximum input length errors by shrin const result = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); - assert.equal(result.embedded, 4); + assert.equal(result.embedded, 2); const shortenedAttempts = Array.from( new Set( embedCalls @@ -1627,7 +1643,7 @@ test('tui snapshot returns mixed issue and pull request counts with default rece assert.equal(snapshot.stats.lastGithubReconciliationAt, '2026-03-09T12:00:00Z'); assert.equal(snapshot.stats.lastEmbedRefreshAt, '2026-03-09T13:00:00Z'); assert.equal(snapshot.stats.staleEmbedThreadCount, 5); - assert.equal(snapshot.stats.staleEmbedSourceCount, 10); + assert.equal(snapshot.stats.staleEmbedSourceCount, 5); assert.equal(snapshot.stats.latestClusterRunId, 1); assert.equal(snapshot.clusters.length, 0); @@ -1865,7 +1881,7 @@ test('refreshRepository runs sync, embed, and cluster in order and returns the c summarizeThread: async () => { throw new Error('not expected'); }, - embedTexts: async ({ texts }) => texts.map(() => [1, 0]), + embedTexts: async ({ texts }) => texts.map((_text, index) => makeEmbedding(1, index)), }, ); @@ -1880,7 +1896,7 @@ test('refreshRepository runs sync, embed, and cluster in order and returns the c assert.equal(result.selected.embed, true); assert.equal(result.selected.cluster, true); assert.equal(result.sync?.threadsSynced, 1); - assert.equal(result.embed?.embedded, 2); + assert.equal(result.embed?.embedded, 1); assert.equal(result.cluster?.clusters, 1); const syncIndex = messages.findIndex((message) => message.includes('[sync]')); diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index 98e2b94..a36958e 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -57,6 +57,7 @@ import { loadConfig, requireGithubToken, requireOpenAiKey, + type EmbeddingBasis, type ConfigValueSource, type GitcrawlConfig, } from './config.js'; @@ -66,6 +67,8 @@ import { buildCanonicalDocument, isBotLikeAuthor } from './documents/normalize.j import { makeGitHubClient, type GitHubClient } from './github/client.js'; import { OpenAiProvider, type AiProvider } from './openai/provider.js'; import { cosineSimilarity, dotProduct, normalizeEmbedding, rankNearestNeighbors, rankNearestNeighborsByScore } from './search/exact.js'; +import type { VectorStore } from './vector/store.js'; +import { VectorliteStore } from './vector/vectorlite-store.js'; type RunTable = 'sync_runs' | 'summary_runs' | 'embedding_runs' | 'cluster_runs'; @@ -117,6 +120,39 @@ type StoredEmbeddingRow = ThreadRow & { embedding_json: string; }; +type ActiveVectorTask = { + threadId: number; + threadNumber: number; + basis: EmbeddingBasis; + text: string; + contentHash: string; + estimatedTokens: number; + wasTruncated: boolean; +}; + +type ActiveVectorRow = ThreadRow & { + basis: EmbeddingBasis; + model: string; + dimensions: number; + content_hash: string; + vector_json: string; + vector_backend: string; +}; + +type RepoPipelineStateRow = { + repo_id: number; + summary_model: string; + summary_prompt_version: string; + embedding_basis: EmbeddingBasis; + embed_model: string; + embed_dimensions: number; + embed_pipeline_version: string; + vector_backend: string; + vectors_current_at: string | null; + clusters_current_at: string | null; + updated_at: string; +}; + type ClusterExperimentMemoryStats = { rssBeforeBytes: number; rssAfterBytes: number; @@ -172,9 +208,10 @@ type EmbeddingWorkset = { title: string; body: string | null; }>; - tasks: EmbeddingTask[]; + tasks: ActiveVectorTask[]; existing: Map; - pending: EmbeddingTask[]; + pending: ActiveVectorTask[]; + missingSummaryThreadNumbers: number[]; }; type SyncCursorState = { @@ -288,6 +325,11 @@ export type DoctorResult = { authOk: boolean; error: string | null; }; + vectorlite: { + configured: boolean; + runtimeOk: boolean; + error: string | null; + }; }; type SyncOptions = { @@ -317,6 +359,9 @@ const EMBED_TRUNCATION_MARKER = '\n\n[truncated for embedding]'; const EMBED_CONTEXT_RETRY_ATTEMPTS = 5; const EMBED_CONTEXT_RETRY_FALLBACK_SHRINK_RATIO = 0.9; const EMBED_CONTEXT_RETRY_TARGET_BUFFER_RATIO = 0.95; +const SUMMARY_PROMPT_VERSION = 'v1'; +const ACTIVE_EMBED_DIMENSIONS = 1024; +const ACTIVE_EMBED_PIPELINE_VERSION = 'vectorlite-1024-v1'; function nowIso(): string { return new Date().toISOString(); @@ -480,12 +525,14 @@ export class GHCrawlService { readonly db: SqliteDatabase; readonly github?: GitHubClient; readonly ai?: AiProvider; + readonly vectorStore: VectorStore; constructor(options: { config?: GitcrawlConfig; db?: SqliteDatabase; github?: GitHubClient; ai?: AiProvider; + vectorStore?: VectorStore; } = {}) { this.config = options.config ?? loadConfig(); ensureRuntimeDirs(this.config); @@ -493,9 +540,11 @@ export class GHCrawlService { migrate(this.db); this.github = options.github ?? (this.config.githubToken ? makeGitHubClient({ token: this.config.githubToken }) : undefined); this.ai = options.ai ?? (this.config.openaiApiKey ? new OpenAiProvider(this.config.openaiApiKey) : undefined); + this.vectorStore = options.vectorStore ?? new VectorliteStore(); } close(): void { + this.vectorStore.close(); this.db.close(); } @@ -562,7 +611,18 @@ export class GHCrawlService { } } - return { health, github, openai }; + const vectorliteHealth = this.vectorStore.checkRuntime(); + + return { + health, + github, + openai, + vectorlite: { + configured: this.config.vectorBackend === 'vectorlite', + runtimeOk: vectorliteHealth.ok, + error: vectorliteHealth.error, + }, + }; } listRepositories(): RepositoriesResponse { @@ -982,10 +1042,12 @@ export class GHCrawlService { const pending = sources.filter((row) => { const latest = this.db .prepare( - 'select content_hash from document_summaries where thread_id = ? and summary_kind = ? and model = ? limit 1', + 'select content_hash, prompt_version from document_summaries where thread_id = ? and summary_kind = ? and model = ? limit 1', ) - .get(row.id, 'dedupe_summary', this.config.summaryModel) as { content_hash: string } | undefined; - return latest?.content_hash !== row.summaryContentHash; + .get(row.id, 'dedupe_summary', this.config.summaryModel) as + | { content_hash: string; prompt_version: string | null } + | undefined; + return latest?.content_hash !== row.summaryContentHash || latest?.prompt_version !== SUMMARY_PROMPT_VERSION; }); params.onProgress?.( @@ -1131,22 +1193,32 @@ export class GHCrawlService { const runId = this.startRun('embedding_runs', repository.id, params.threadNumber ? `thread:${params.threadNumber}` : repository.fullName); try { - const { rows, tasks, pending } = this.getEmbeddingWorkset(repository.id, params.threadNumber); + if (!this.isRepoVectorStateCurrent(repository.id) && params.threadNumber === undefined) { + this.resetRepositoryVectors(repository.id, repository.fullName); + } + + const { rows, tasks, pending, missingSummaryThreadNumbers } = this.getEmbeddingWorkset(repository.id, params.threadNumber); const skipped = tasks.length - pending.length; const truncated = tasks.filter((task) => task.wasTruncated).length; + if (missingSummaryThreadNumbers.length > 0) { + throw new Error( + `Embedding basis ${this.config.embeddingBasis} requires summaries before embedding. Missing summaries for thread(s): ${missingSummaryThreadNumbers.slice(0, 10).join(', ')}${missingSummaryThreadNumbers.length > 10 ? ', …' : ''}.`, + ); + } + params.onProgress?.( - `[embed] loaded ${rows.length} open thread(s) and ${tasks.length} embedding source(s) for ${repository.fullName}`, + `[embed] loaded ${rows.length} open thread(s) and ${tasks.length} active vector task(s) for ${repository.fullName}`, ); params.onProgress?.( - `[embed] pending=${pending.length} skipped=${skipped} truncated=${truncated} model=${this.config.embedModel} batch_size=${this.config.embedBatchSize} concurrency=${this.config.embedConcurrency} max_unread=${this.config.embedMaxUnread} max_batch_tokens=${EMBED_MAX_BATCH_TOKENS}`, + `[embed] pending=${pending.length} skipped=${skipped} truncated=${truncated} model=${this.config.embedModel} dimensions=${ACTIVE_EMBED_DIMENSIONS} basis=${this.config.embeddingBasis} batch_size=${this.config.embedBatchSize} concurrency=${this.config.embedConcurrency} max_unread=${this.config.embedMaxUnread} max_batch_tokens=${EMBED_MAX_BATCH_TOKENS}`, ); let embedded = 0; const batches = this.chunkEmbeddingTasks(pending, this.config.embedBatchSize, EMBED_MAX_BATCH_TOKENS); const mapper = new IterableMapper( batches, - async (batch: EmbeddingTask[]) => { + async (batch: ActiveVectorTask[]) => { return this.embedBatchWithRecovery(ai, batch, params.onProgress); }, { @@ -1158,17 +1230,18 @@ export class GHCrawlService { let completedBatches = 0; for await (const batchResult of mapper) { completedBatches += 1; - const numbers = batchResult.map(({ task }) => `#${task.threadNumber}:${task.sourceKind}`); + const numbers = batchResult.map(({ task }) => `#${task.threadNumber}:${task.basis}`); const estimatedTokens = batchResult.reduce((sum, { task }) => sum + task.estimatedTokens, 0); params.onProgress?.( `[embed] batch ${completedBatches}/${Math.max(batches.length, 1)} size=${batchResult.length} est_tokens=${estimatedTokens} items=${numbers.join(',')}`, ); for (const { task, embedding } of batchResult) { - this.upsertEmbedding(task.threadId, task.sourceKind, task.contentHash, embedding); + this.upsertActiveVector(repository.id, repository.fullName, task.threadId, task.basis, task.contentHash, embedding); embedded += 1; } } + this.markRepoVectorsCurrent(repository.id); this.finishRun('embedding_runs', runId, 'completed', { embedded }); return embedResultSchema.parse({ runId, embedded }); } catch (error) { @@ -1190,16 +1263,68 @@ export class GHCrawlService { const k = params.k ?? 6; try { - const { items, sourceKinds } = this.loadClusterableThreadMeta(repository.id); + let items: Array<{ id: number; number: number; title: string }>; + let aggregatedEdges: Map }>; + + if (this.isRepoVectorStateCurrent(repository.id)) { + const vectorItems = this.loadClusterableActiveVectorMeta(repository.id); + const activeIds = new Set(vectorItems.map((item) => item.id)); + const candidateK = Math.max(k * 16, 64); + aggregatedEdges = new Map(); + let processed = 0; + let lastProgressAt = Date.now(); + + params.onProgress?.( + `[cluster] loaded ${vectorItems.length} active vector(s) for ${repository.fullName} backend=${this.config.vectorBackend} k=${k} candidateK=${candidateK} minScore=${minScore}`, + ); + for (const item of vectorItems) { + const neighbors = this.vectorStore.queryNearest({ + storePath: this.repoVectorStorePath(repository.fullName), + dimensions: ACTIVE_EMBED_DIMENSIONS, + vector: item.embedding, + limit: k, + candidateK: candidateK + 1, + excludeThreadId: item.id, + }); + for (const neighbor of neighbors) { + if (!activeIds.has(neighbor.threadId)) continue; + if (neighbor.score < minScore) continue; + const key = this.edgeKey(item.id, neighbor.threadId); + const existing = aggregatedEdges.get(key); + if (existing) { + existing.score = Math.max(existing.score, neighbor.score); + } else { + aggregatedEdges.set(key, { + leftThreadId: Math.min(item.id, neighbor.threadId), + rightThreadId: Math.max(item.id, neighbor.threadId), + score: neighbor.score, + sourceKinds: new Set(['dedupe_summary']), + }); + } + } + processed += 1; + const now = Date.now(); + if (params.onProgress && now - lastProgressAt >= CLUSTER_PROGRESS_INTERVAL_MS) { + params.onProgress(`[cluster] queried ${processed}/${vectorItems.length} vectors current_edges=${aggregatedEdges.size}`); + lastProgressAt = now; + } + } + items = vectorItems; + } else if (this.hasLegacyEmbeddings(repository.id)) { + const legacy = this.loadClusterableThreadMeta(repository.id); + items = legacy.items; + params.onProgress?.( + `[cluster] loaded ${items.length} legacy embedded thread(s) across ${legacy.sourceKinds.length} source kind(s) for ${repository.fullName} k=${k} minScore=${minScore}`, + ); + aggregatedEdges = await this.aggregateRepositoryEdges(repository.id, legacy.sourceKinds, { + limit: k, + minScore, + onProgress: params.onProgress, + }); + } else { + throw new Error(`Vectors for ${repository.fullName} are stale or missing. Run refresh or embed first.`); + } - params.onProgress?.( - `[cluster] loaded ${items.length} embedded thread(s) across ${sourceKinds.length} source kind(s) for ${repository.fullName} k=${k} minScore=${minScore}`, - ); - const aggregatedEdges = await this.aggregateRepositoryEdges(repository.id, sourceKinds, { - limit: k, - minScore, - onProgress: params.onProgress, - }); const edges = Array.from(aggregatedEdges.values()).map((entry) => ({ leftThreadId: entry.leftThreadId, rightThreadId: entry.rightThreadId, @@ -1214,6 +1339,9 @@ export class GHCrawlService { ); this.persistClusterRun(repository.id, runId, aggregatedEdges, clusters); this.pruneOldClusterRuns(repository.id, runId); + if (this.isRepoVectorStateCurrent(repository.id)) { + this.markRepoClustersCurrent(repository.id); + } params.onProgress?.(`[cluster] persisted ${clusters.length} cluster(s) and pruned older cluster runs`); @@ -1517,11 +1645,30 @@ export class GHCrawlService { } if (mode !== 'keyword' && this.ai) { - const [queryEmbedding] = await this.ai.embedTexts({ model: this.config.embedModel, texts: [params.query] }); - for (const row of this.iterateStoredEmbeddings(repository.id)) { - const score = cosineSimilarity(queryEmbedding, JSON.parse(row.embedding_json) as number[]); - if (score < 0.2) continue; - semanticScores.set(row.id, Math.max(semanticScores.get(row.id) ?? -1, score)); + if (this.isRepoVectorStateCurrent(repository.id)) { + const [queryEmbedding] = await this.ai.embedTexts({ + model: this.config.embedModel, + texts: [params.query], + dimensions: ACTIVE_EMBED_DIMENSIONS, + }); + const neighbors = this.vectorStore.queryNearest({ + storePath: this.repoVectorStorePath(repository.fullName), + dimensions: ACTIVE_EMBED_DIMENSIONS, + vector: queryEmbedding, + limit: limit * 2, + candidateK: Math.max(limit * 8, 64), + }); + for (const neighbor of neighbors) { + if (neighbor.score < 0.2) continue; + semanticScores.set(neighbor.threadId, Math.max(semanticScores.get(neighbor.threadId) ?? -1, neighbor.score)); + } + } else if (this.hasLegacyEmbeddings(repository.id)) { + const [queryEmbedding] = await this.ai.embedTexts({ model: this.config.embedModel, texts: [params.query] }); + for (const row of this.iterateStoredEmbeddings(repository.id)) { + const score = cosineSimilarity(queryEmbedding, JSON.parse(row.embedding_json) as number[]); + if (score < 0.2) continue; + semanticScores.set(row.id, Math.max(semanticScores.get(row.id) ?? -1, score)); + } } } @@ -1617,45 +1764,109 @@ export class GHCrawlService { const limit = params.limit ?? 10; const minScore = params.minScore ?? 0.2; - const targetRows = this.loadStoredEmbeddingsForThreadNumber(repository.id, params.threadNumber); - if (targetRows.length === 0) { - throw new Error( - `Thread #${params.threadNumber} for ${repository.fullName} was not found with an embedding. Run embed first.`, - ); - } - const targetRow = targetRows[0]; - const targetBySource = new Map(); - for (const row of targetRows) { - targetBySource.set(row.source_kind, JSON.parse(row.embedding_json) as number[]); - } - - const aggregated = new Map(); - for (const row of this.iterateStoredEmbeddings(repository.id)) { - if (row.id === targetRow.id) continue; - const targetEmbedding = targetBySource.get(row.source_kind); - if (!targetEmbedding) continue; - const score = cosineSimilarity(targetEmbedding, JSON.parse(row.embedding_json) as number[]); - if (score < minScore) continue; - const previous = aggregated.get(row.id); - if (!previous || score > previous.score) { - aggregated.set(row.id, { number: row.number, kind: row.kind, title: row.title, score }); + const targetRow = this.db + .prepare( + `select t.*, tv.basis, tv.model, tv.dimensions, tv.content_hash, tv.vector_json, tv.vector_backend + from threads t + join thread_vectors tv on tv.thread_id = t.id + where t.repo_id = ? + and t.number = ? + and t.state = 'open' + and t.closed_at_local is null + and tv.model = ? + and tv.basis = ? + and tv.dimensions = ? + limit 1`, + ) + .get( + repository.id, + params.threadNumber, + this.config.embedModel, + this.config.embeddingBasis, + ACTIVE_EMBED_DIMENSIONS, + ) as ActiveVectorRow | undefined; + let responseThread: ThreadRow | ActiveVectorRow; + let neighbors: Array<{ threadId: number; number: number; kind: 'issue' | 'pull_request'; title: string; score: number }>; + + if (targetRow) { + responseThread = targetRow; + const candidateRows = this.vectorStore + .queryNearest({ + storePath: this.repoVectorStorePath(repository.fullName), + dimensions: ACTIVE_EMBED_DIMENSIONS, + vector: JSON.parse(targetRow.vector_json) as number[], + limit: limit * 2, + candidateK: Math.max(limit * 8, 64), + excludeThreadId: targetRow.id, + }) + .filter((row) => row.score >= minScore); + const candidateIds = candidateRows.map((row) => row.threadId); + const neighborMeta = candidateIds.length + ? (this.db + .prepare( + `select * from threads + where repo_id = ? and state = 'open' and closed_at_local is null and id in (${candidateIds.map(() => '?').join(',')})`, + ) + .all(repository.id, ...candidateIds) as ThreadRow[]) + : []; + const metaById = new Map(neighborMeta.map((row) => [row.id, row])); + neighbors = candidateRows + .map((row) => { + const meta = metaById.get(row.threadId); + if (!meta) { + return null; + } + return { + threadId: row.threadId, + number: meta.number, + kind: meta.kind, + title: meta.title, + score: row.score, + }; + }) + .filter((row): row is NonNullable => row !== null) + .slice(0, limit); + } else { + const targetRows = this.loadStoredEmbeddingsForThreadNumber(repository.id, params.threadNumber); + if (targetRows.length === 0) { + throw new Error( + `Thread #${params.threadNumber} for ${repository.fullName} was not found with an embedding. Run embed first.`, + ); + } + responseThread = targetRows[0]!; + const targetBySource = new Map(); + for (const row of targetRows) { + targetBySource.set(row.source_kind, JSON.parse(row.embedding_json) as number[]); } - } - const neighbors = Array.from(aggregated.entries()) - .map(([threadId, value]) => ({ - threadId, - number: value.number, - kind: value.kind, - title: value.title, - score: value.score, - })) - .sort((left, right) => right.score - left.score) - .slice(0, limit); + const aggregated = new Map(); + for (const row of this.iterateStoredEmbeddings(repository.id)) { + if (row.id === responseThread.id) continue; + const targetEmbedding = targetBySource.get(row.source_kind); + if (!targetEmbedding) continue; + const score = cosineSimilarity(targetEmbedding, JSON.parse(row.embedding_json) as number[]); + if (score < minScore) continue; + const previous = aggregated.get(row.id); + if (!previous || score > previous.score) { + aggregated.set(row.id, { number: row.number, kind: row.kind, title: row.title, score }); + } + } + + neighbors = Array.from(aggregated.entries()) + .map(([threadId, value]) => ({ + threadId, + number: value.number, + kind: value.kind, + title: value.title, + score: value.score, + })) + .sort((left, right) => right.score - left.score) + .slice(0, limit); + } return neighborsResponseSchema.parse({ repository, - thread: threadToDto(targetRow), + thread: threadToDto(responseThread), neighbors, }); } @@ -1764,6 +1975,16 @@ export class GHCrawlService { onProgress: params.onProgress, }); } + if (selected.embed && this.config.embeddingBasis === 'title_summary') { + params.onProgress?.( + `[refresh] embedding basis ${this.config.embeddingBasis} requires summaries; running summarize before embed`, + ); + await this.summarizeRepository({ + owner: params.owner, + repo: params.repo, + onProgress: params.onProgress, + }); + } if (selected.embed) { embed = await this.embedRepository({ owner: params.owner, @@ -2029,10 +2250,10 @@ export class GHCrawlService { .prepare( `select summary_kind, summary_text from document_summaries - where thread_id = ? and model = ? + where thread_id = ? and model = ? and prompt_version = ? order by summary_kind asc`, ) - .all(row.id, this.config.summaryModel) as Array<{ summary_kind: string; summary_text: string }>; + .all(row.id, this.config.summaryModel, SUMMARY_PROMPT_VERSION) as Array<{ summary_kind: string; summary_text: string }>; const summaries: TuiThreadDetail['summaries'] = {}; for (const summary of summaryRows) { if ( @@ -2227,7 +2448,152 @@ export class GHCrawlService { }; } + private getDesiredPipelineState(): Omit { + return { + summary_model: this.config.summaryModel, + summary_prompt_version: SUMMARY_PROMPT_VERSION, + embedding_basis: this.config.embeddingBasis, + embed_model: this.config.embedModel, + embed_dimensions: ACTIVE_EMBED_DIMENSIONS, + embed_pipeline_version: ACTIVE_EMBED_PIPELINE_VERSION, + vector_backend: this.config.vectorBackend, + }; + } + + private getRepoPipelineState(repoId: number): RepoPipelineStateRow | null { + return ( + (this.db.prepare('select * from repo_pipeline_state where repo_id = ? limit 1').get(repoId) as RepoPipelineStateRow | undefined) ?? + null + ); + } + + private isRepoVectorStateCurrent(repoId: number): boolean { + const state = this.getRepoPipelineState(repoId); + if (!state || !state.vectors_current_at) { + return false; + } + const desired = this.getDesiredPipelineState(); + return ( + state.summary_model === desired.summary_model && + state.summary_prompt_version === desired.summary_prompt_version && + state.embedding_basis === desired.embedding_basis && + state.embed_model === desired.embed_model && + state.embed_dimensions === desired.embed_dimensions && + state.embed_pipeline_version === desired.embed_pipeline_version && + state.vector_backend === desired.vector_backend + ); + } + + private isRepoClusterStateCurrent(repoId: number): boolean { + const state = this.getRepoPipelineState(repoId); + return this.isRepoVectorStateCurrent(repoId) && Boolean(state?.clusters_current_at); + } + + private hasLegacyEmbeddings(repoId: number): boolean { + const row = this.db + .prepare( + `select count(*) as count + from document_embeddings e + join threads t on t.id = e.thread_id + where t.repo_id = ? + and t.state = 'open' + and t.closed_at_local is null + and e.model = ?`, + ) + .get(repoId, this.config.embedModel) as { count: number }; + return row.count > 0; + } + + private writeRepoPipelineState( + repoId: number, + overrides: Partial>, + ): void { + const desired = this.getDesiredPipelineState(); + const current = this.getRepoPipelineState(repoId); + this.db + .prepare( + `insert into repo_pipeline_state ( + repo_id, + summary_model, + summary_prompt_version, + embedding_basis, + embed_model, + embed_dimensions, + embed_pipeline_version, + vector_backend, + vectors_current_at, + clusters_current_at, + updated_at + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + on conflict(repo_id) do update set + summary_model = excluded.summary_model, + summary_prompt_version = excluded.summary_prompt_version, + embedding_basis = excluded.embedding_basis, + embed_model = excluded.embed_model, + embed_dimensions = excluded.embed_dimensions, + embed_pipeline_version = excluded.embed_pipeline_version, + vector_backend = excluded.vector_backend, + vectors_current_at = excluded.vectors_current_at, + clusters_current_at = excluded.clusters_current_at, + updated_at = excluded.updated_at`, + ) + .run( + repoId, + desired.summary_model, + desired.summary_prompt_version, + desired.embedding_basis, + desired.embed_model, + desired.embed_dimensions, + desired.embed_pipeline_version, + desired.vector_backend, + overrides.vectors_current_at ?? current?.vectors_current_at ?? null, + overrides.clusters_current_at ?? current?.clusters_current_at ?? null, + nowIso(), + ); + } + + private markRepoVectorsCurrent(repoId: number): void { + this.writeRepoPipelineState(repoId, { + vectors_current_at: nowIso(), + clusters_current_at: null, + }); + } + + private markRepoClustersCurrent(repoId: number): void { + const state = this.getRepoPipelineState(repoId); + this.writeRepoPipelineState(repoId, { + vectors_current_at: state?.vectors_current_at ?? nowIso(), + clusters_current_at: nowIso(), + }); + } + + private repoVectorStorePath(repoFullName: string): string { + const safeName = repoFullName.replace(/[^a-zA-Z0-9._-]+/g, '__'); + return path.join(this.config.configDir, 'vectors', `${safeName}.sqlite`); + } + + private resetRepositoryVectors(repoId: number, repoFullName: string): void { + this.db + .prepare( + `delete from thread_vectors + where thread_id in (select id from threads where repo_id = ?)`, + ) + .run(repoId); + this.vectorStore.resetRepository({ + storePath: this.repoVectorStorePath(repoFullName), + dimensions: ACTIVE_EMBED_DIMENSIONS, + }); + this.writeRepoPipelineState(repoId, { + vectors_current_at: null, + clusters_current_at: null, + }); + } + private getLatestClusterRun(repoId: number): { id: number; finished_at: string | null } | null { + const state = this.getRepoPipelineState(repoId); + if (state && !this.isRepoClusterStateCurrent(repoId)) { + return null; + } return ( (this.db .prepare("select id, finished_at from cluster_runs where repo_id = ? and status = 'completed' order by id desc limit 1") @@ -2928,7 +3294,9 @@ export class GHCrawlService { } const summaryInput = parts.join('\n\n'); - const summaryContentHash = stableContentHash(`summary:${includeComments ? 'with-comments' : 'metadata-only'}\n${summaryInput}`); + const summaryContentHash = stableContentHash( + `summary:${SUMMARY_PROMPT_VERSION}:${includeComments ? 'with-comments' : 'metadata-only'}\n${summaryInput}`, + ); return { summaryInput, summaryContentHash }; } @@ -2982,6 +3350,45 @@ export class GHCrawlService { return tasks; } + private buildActiveVectorTask(params: { + threadId: number; + threadNumber: number; + title: string; + body: string | null; + dedupeSummary: string | null; + }): ActiveVectorTask | null { + const sections = [`title: ${normalizeSummaryText(params.title)}`]; + if (this.config.embeddingBasis === 'title_summary') { + const summary = normalizeSummaryText(params.dedupeSummary ?? ''); + if (!summary) { + return null; + } + sections.push(`summary: ${summary}`); + } else { + const body = normalizeSummaryText(params.body ?? ''); + if (body) { + sections.push(`body: ${body}`); + } + } + + const prepared = this.prepareEmbeddingText(sections.join('\n\n'), EMBED_MAX_ITEM_TOKENS); + if (!prepared) { + return null; + } + + return { + threadId: params.threadId, + threadNumber: params.threadNumber, + basis: this.config.embeddingBasis, + text: prepared.text, + contentHash: stableContentHash( + `embedding:${ACTIVE_EMBED_PIPELINE_VERSION}:${this.config.embeddingBasis}:${this.config.embedModel}:${ACTIVE_EMBED_DIMENSIONS}\n${prepared.text}`, + ), + estimatedTokens: prepared.estimatedTokens, + wasTruncated: prepared.wasTruncated, + }; + } + private prepareEmbeddingText( text: string, maxEstimatedTokens: number, @@ -3030,13 +3437,14 @@ export class GHCrawlService { private async embedBatchWithRecovery( ai: AiProvider, - batch: EmbeddingTask[], + batch: ActiveVectorTask[], onProgress?: (message: string) => void, - ): Promise> { + ): Promise> { try { const embeddings = await ai.embedTexts({ model: this.config.embedModel, texts: batch.map((task) => task.text), + dimensions: ACTIVE_EMBED_DIMENSIONS, }); return batch.map((task, index) => ({ task, embedding: embeddings[index] })); } catch (error) { @@ -3052,7 +3460,7 @@ export class GHCrawlService { `[embed] batch context error; isolating ${batch.length} item(s) to find oversized input(s)`, ); - const recovered: Array<{ task: EmbeddingTask; embedding: number[] }> = []; + const recovered: Array<{ task: ActiveVectorTask; embedding: number[] }> = []; for (const task of batch) { recovered.push(await this.embedSingleTaskWithRecovery(ai, task, onProgress)); } @@ -3062,9 +3470,9 @@ export class GHCrawlService { private async embedSingleTaskWithRecovery( ai: AiProvider, - task: EmbeddingTask, + task: ActiveVectorTask, onProgress?: (message: string) => void, - ): Promise<{ task: EmbeddingTask; embedding: number[] }> { + ): Promise<{ task: ActiveVectorTask; embedding: number[] }> { let current = task; for (let attempt = 0; attempt < EMBED_CONTEXT_RETRY_ATTEMPTS; attempt += 1) { @@ -3072,6 +3480,7 @@ export class GHCrawlService { const [embedding] = await ai.embedTexts({ model: this.config.embedModel, texts: [current.text], + dimensions: ACTIVE_EMBED_DIMENSIONS, }); return { task: current, embedding }; } catch (error) { @@ -3085,19 +3494,19 @@ export class GHCrawlService { throw error; } onProgress?.( - `[embed] shortened #${current.threadNumber}:${current.sourceKind} after context error est_tokens=${current.estimatedTokens}->${next.estimatedTokens}`, + `[embed] shortened #${current.threadNumber}:${current.basis} after context error est_tokens=${current.estimatedTokens}->${next.estimatedTokens}`, ); current = next; } } - throw new Error(`Unable to shrink embedding input for #${task.threadNumber}:${task.sourceKind} below model limits`); + throw new Error(`Unable to shrink embedding input for #${task.threadNumber}:${task.basis} below model limits`); } private shrinkEmbeddingTask( - task: EmbeddingTask, + task: ActiveVectorTask, context?: { limitTokens: number | null; requestedTokens: number | null }, - ): EmbeddingTask | null { + ): ActiveVectorTask | null { const withoutMarker = task.text.endsWith(EMBED_TRUNCATION_MARKER) ? task.text.slice(0, -EMBED_TRUNCATION_MARKER.length) : task.text; @@ -3116,7 +3525,9 @@ export class GHCrawlService { return { ...task, text: nextText, - contentHash: stableContentHash(`embedding:${task.sourceKind}\n${nextText}`), + contentHash: stableContentHash( + `embedding:${ACTIVE_EMBED_PIPELINE_VERSION}:${task.basis}:${this.config.embedModel}:${ACTIVE_EMBED_DIMENSIONS}\n${nextText}`, + ), estimatedTokens: this.estimateEmbeddingTokens(nextText), wasTruncated: true, }; @@ -3142,9 +3553,9 @@ export class GHCrawlService { return Math.floor(textLength * EMBED_CONTEXT_RETRY_FALLBACK_SHRINK_RATIO); } - private chunkEmbeddingTasks(items: EmbeddingTask[], maxItems: number, maxEstimatedTokens: number): EmbeddingTask[][] { - const chunks: EmbeddingTask[][] = []; - let current: EmbeddingTask[] = []; + private chunkEmbeddingTasks(items: ActiveVectorTask[], maxItems: number, maxEstimatedTokens: number): ActiveVectorTask[][] { + const chunks: ActiveVectorTask[][] = []; + let current: ActiveVectorTask[] = []; let currentEstimatedTokens = 0; for (const item of items) { @@ -3325,6 +3736,34 @@ export class GHCrawlService { }; } + private loadClusterableActiveVectorMeta(repoId: number): Array<{ id: number; number: number; title: string; embedding: number[] }> { + const rows = this.db + .prepare( + `select t.id, t.number, t.title, tv.vector_json + from threads t + join thread_vectors tv on tv.thread_id = t.id + where t.repo_id = ? + and t.state = 'open' + and t.closed_at_local is null + and tv.model = ? + and tv.basis = ? + and tv.dimensions = ? + order by t.number asc`, + ) + .all(repoId, this.config.embedModel, this.config.embeddingBasis, ACTIVE_EMBED_DIMENSIONS) as Array<{ + id: number; + number: number; + title: string; + vector_json: string; + }>; + return rows.map((row) => ({ + id: row.id, + number: row.number, + title: row.title, + embedding: JSON.parse(row.vector_json) as number[], + })); + } + private listStoredClusterNeighbors(repoId: number, threadId: number, limit: number): SearchHitDto['neighbors'] { const latestRun = this.getLatestClusterRun(repoId); if (!latestRun) { @@ -3398,72 +3837,76 @@ export class GHCrawlService { title: string; body: string | null; }>; - const summaryTexts = this.loadCombinedSummaryTextMap(repoId, threadNumber); - const tasks = rows.flatMap((row) => - this.buildEmbeddingTasks({ + const summaryTexts = this.loadDedupeSummaryTextMap(repoId, threadNumber); + const missingSummaryThreadNumbers: number[] = []; + const tasks = rows.flatMap((row) => { + const task = this.buildActiveVectorTask({ threadId: row.id, threadNumber: row.number, title: row.title, body: row.body, dedupeSummary: summaryTexts.get(row.id) ?? null, - }), - ); + }); + if (task) { + return [task]; + } + if (this.config.embeddingBasis === 'title_summary') { + missingSummaryThreadNumbers.push(row.number); + } + return []; + }); + const pipelineCurrent = this.isRepoVectorStateCurrent(repoId); const existingRows = this.db .prepare( - `select e.thread_id, e.source_kind, e.content_hash - from document_embeddings e - join threads t on t.id = e.thread_id - where t.repo_id = ? and e.model = ?`, + `select tv.thread_id, tv.content_hash + from thread_vectors tv + join threads t on t.id = tv.thread_id + where t.repo_id = ? + and tv.model = ? + and tv.basis = ? + and tv.dimensions = ?`, ) - .all(repoId, this.config.embedModel) as Array<{ + .all(repoId, this.config.embedModel, this.config.embeddingBasis, ACTIVE_EMBED_DIMENSIONS) as Array<{ thread_id: number; - source_kind: EmbeddingSourceKind; content_hash: string; }>; const existing = new Map(); for (const row of existingRows) { - existing.set(`${row.thread_id}:${row.source_kind}`, row.content_hash); + existing.set(String(row.thread_id), row.content_hash); } - const pending = tasks.filter((task) => existing.get(`${task.threadId}:${task.sourceKind}`) !== task.contentHash); - return { rows, tasks, existing, pending }; + const pending = pipelineCurrent + ? tasks.filter((task) => existing.get(String(task.threadId)) !== task.contentHash) + : tasks; + return { rows, tasks, existing, pending, missingSummaryThreadNumbers }; } - private loadCombinedSummaryTextMap(repoId: number, threadNumber?: number): Map { + private loadDedupeSummaryTextMap(repoId: number, threadNumber?: number): Map { let sql = - `select s.thread_id, s.summary_kind, s.summary_text + `select s.thread_id, s.summary_text from document_summaries s join threads t on t.id = s.thread_id - where t.repo_id = ? and t.state = 'open' and t.closed_at_local is null and s.model = ?`; - const args: Array = [repoId, this.config.summaryModel]; + where t.repo_id = ? + and t.state = 'open' + and t.closed_at_local is null + and s.model = ? + and s.summary_kind = 'dedupe_summary' + and s.prompt_version = ?`; + const args: Array = [repoId, this.config.summaryModel, SUMMARY_PROMPT_VERSION]; if (threadNumber) { sql += ' and t.number = ?'; args.push(threadNumber); } - sql += ' order by t.number asc, s.summary_kind asc'; + sql += ' order by t.number asc'; const rows = this.db.prepare(sql).all(...args) as Array<{ thread_id: number; - summary_kind: string; summary_text: string; }>; - const byThread = new Map>(); - for (const row of rows) { - const entry = byThread.get(row.thread_id) ?? new Map(); - entry.set(row.summary_kind, normalizeSummaryText(row.summary_text)); - byThread.set(row.thread_id, entry); - } - const combined = new Map(); - const order = ['problem_summary', 'solution_summary', 'maintainer_signal_summary', 'dedupe_summary']; - for (const [threadId, entry] of byThread.entries()) { - const parts = order - .map((summaryKind) => { - const text = entry.get(summaryKind); - return text ? `${summaryKind}: ${text}` : ''; - }) - .filter(Boolean); - if (parts.length > 0) { - combined.set(threadId, parts.join('\n\n')); + for (const row of rows) { + const text = normalizeSummaryText(row.summary_text); + if (text) { + combined.set(row.thread_id, text); } } return combined; @@ -3791,14 +4234,55 @@ export class GHCrawlService { private upsertSummary(threadId: number, contentHash: string, summaryKind: string, summaryText: string): void { this.db .prepare( - `insert into document_summaries (thread_id, summary_kind, model, content_hash, summary_text, created_at, updated_at) - values (?, ?, ?, ?, ?, ?, ?) + `insert into document_summaries (thread_id, summary_kind, model, prompt_version, content_hash, summary_text, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?) on conflict(thread_id, summary_kind, model) do update set + prompt_version = excluded.prompt_version, content_hash = excluded.content_hash, summary_text = excluded.summary_text, updated_at = excluded.updated_at`, ) - .run(threadId, summaryKind, this.config.summaryModel, contentHash, summaryText, nowIso(), nowIso()); + .run(threadId, summaryKind, this.config.summaryModel, SUMMARY_PROMPT_VERSION, contentHash, summaryText, nowIso(), nowIso()); + } + + private upsertActiveVector( + repoId: number, + repoFullName: string, + threadId: number, + basis: EmbeddingBasis, + contentHash: string, + embedding: number[], + ): void { + this.db + .prepare( + `insert into thread_vectors (thread_id, basis, model, dimensions, content_hash, vector_json, vector_backend, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?) + on conflict(thread_id) do update set + basis = excluded.basis, + model = excluded.model, + dimensions = excluded.dimensions, + content_hash = excluded.content_hash, + vector_json = excluded.vector_json, + vector_backend = excluded.vector_backend, + updated_at = excluded.updated_at`, + ) + .run( + threadId, + basis, + this.config.embedModel, + embedding.length, + contentHash, + asJson(embedding), + this.config.vectorBackend, + nowIso(), + nowIso(), + ); + this.vectorStore.upsertVector({ + storePath: this.repoVectorStorePath(repoFullName), + dimensions: ACTIVE_EMBED_DIMENSIONS, + threadId, + vector: embedding, + }); } private upsertEmbedding(threadId: number, sourceKind: EmbeddingSourceKind, contentHash: string, embedding: number[]): void { diff --git a/packages/api-core/src/vector/store.ts b/packages/api-core/src/vector/store.ts new file mode 100644 index 0000000..73a6fa0 --- /dev/null +++ b/packages/api-core/src/vector/store.ts @@ -0,0 +1,28 @@ +export type VectorStoreHealth = { + ok: boolean; + error: string | null; +}; + +export type VectorNeighbor = { + threadId: number; + score: number; +}; + +export type VectorQueryParams = { + storePath: string; + dimensions: number; + vector: number[]; + limit: number; + candidateK?: number; + excludeThreadId?: number; + efSearch?: number; +}; + +export type VectorStore = { + checkRuntime: () => VectorStoreHealth; + resetRepository: (params: { storePath: string; dimensions: number }) => void; + upsertVector: (params: { storePath: string; dimensions: number; threadId: number; vector: number[] }) => void; + deleteVector: (params: { storePath: string; dimensions: number; threadId: number }) => void; + queryNearest: (params: VectorQueryParams) => VectorNeighbor[]; + close: () => void; +}; diff --git a/packages/api-core/src/vector/vectorlite-store.test.ts b/packages/api-core/src/vector/vectorlite-store.test.ts new file mode 100644 index 0000000..c7eab8e --- /dev/null +++ b/packages/api-core/src/vector/vectorlite-store.test.ts @@ -0,0 +1,88 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { VectorliteStore } from './vectorlite-store.js'; + +function makeStorePath(): string { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ghcrawl-vector-store-test-')); + return path.join(dir, 'repo.sqlite'); +} + +test('vectorlite store persists vectors across reopen', () => { + const storePath = makeStorePath(); + const vector = [1, 0, 0, 0]; + const neighbor = [0.9, 0.1, 0, 0]; + const far = [0, 1, 0, 0]; + + const first = new VectorliteStore(); + try { + const health = first.checkRuntime(); + assert.equal(health.ok, true); + first.upsertVector({ storePath, dimensions: 4, threadId: 1, vector }); + first.upsertVector({ storePath, dimensions: 4, threadId: 2, vector: neighbor }); + first.upsertVector({ storePath, dimensions: 4, threadId: 3, vector: far }); + } finally { + first.close(); + } + + const reopened = new VectorliteStore(); + try { + const results = reopened.queryNearest({ + storePath, + dimensions: 4, + vector, + limit: 2, + excludeThreadId: 1, + candidateK: 3, + }); + assert.deepEqual(results.map((row) => row.threadId), [2, 3]); + assert.ok(results[0]!.score > results[1]!.score); + } finally { + reopened.close(); + } +}); + +test('vectorlite store update and delete affect later queries', () => { + const storePath = makeStorePath(); + const store = new VectorliteStore(); + try { + store.upsertVector({ storePath, dimensions: 3, threadId: 1, vector: [1, 0, 0] }); + store.upsertVector({ storePath, dimensions: 3, threadId: 2, vector: [0.8, 0.2, 0] }); + let results = store.queryNearest({ + storePath, + dimensions: 3, + vector: [1, 0, 0], + limit: 1, + excludeThreadId: 1, + candidateK: 2, + }); + assert.deepEqual(results.map((row) => row.threadId), [2]); + + store.upsertVector({ storePath, dimensions: 3, threadId: 2, vector: [0, 1, 0] }); + results = store.queryNearest({ + storePath, + dimensions: 3, + vector: [1, 0, 0], + limit: 1, + excludeThreadId: 1, + candidateK: 2, + }); + assert.ok(results[0]!.score < 0.5); + + store.deleteVector({ storePath, dimensions: 3, threadId: 2 }); + results = store.queryNearest({ + storePath, + dimensions: 3, + vector: [1, 0, 0], + limit: 1, + excludeThreadId: 1, + candidateK: 2, + }); + assert.deepEqual(results, []); + } finally { + store.close(); + } +}); diff --git a/packages/api-core/src/vector/vectorlite-store.ts b/packages/api-core/src/vector/vectorlite-store.ts new file mode 100644 index 0000000..dfdb3cb --- /dev/null +++ b/packages/api-core/src/vector/vectorlite-store.ts @@ -0,0 +1,159 @@ +import { createRequire } from 'node:module'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { openDb, type SqliteDatabase } from '../db/sqlite.js'; +import type { VectorNeighbor, VectorQueryParams, VectorStore, VectorStoreHealth } from './store.js'; + +const requireFromHere = createRequire(import.meta.url); +const TABLE_NAME = 'thread_vectors_ann'; +const META_TABLE_NAME = 'vector_store_meta'; +const HNSW_MAX_ELEMENTS = 1_000_000; + +type SqliteWithExtension = SqliteDatabase & { + loadExtension: (extensionPath: string) => void; +}; + +type StoreHandle = { + db: SqliteWithExtension; + storePath: string; + dimensions: number | null; +}; + +export class VectorliteStore implements VectorStore { + private readonly handles = new Map(); + + constructor( + private readonly options: { + extensionPathProvider?: () => string; + } = {}, + ) {} + + checkRuntime(): VectorStoreHealth { + try { + this.resolveExtensionPath(); + const db = openDb(':memory:') as SqliteWithExtension; + try { + db.loadExtension(this.resolveExtensionPath()); + db.prepare('select vectorlite_info()').get(); + } finally { + db.close(); + } + return { ok: true, error: null }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + resetRepository(params: { storePath: string; dimensions: number }): void { + const handle = this.getHandle(params.storePath, params.dimensions); + handle.db.exec(`drop table if exists ${TABLE_NAME}`); + handle.db.exec(`delete from ${META_TABLE_NAME}`); + fs.rmSync(this.indexPath(params.storePath), { force: true }); + handle.dimensions = null; + this.ensureSchema(handle, params.dimensions); + } + + upsertVector(params: { storePath: string; dimensions: number; threadId: number; vector: number[] }): void { + const handle = this.getHandle(params.storePath, params.dimensions); + handle.db.exec(`delete from ${TABLE_NAME} where rowid = ${Math.trunc(params.threadId)}`); + handle.db + .prepare(`insert into ${TABLE_NAME}(rowid, vec) values (?, ?)`) + .run(params.threadId, this.vectorBuffer(params.vector)); + } + + deleteVector(params: { storePath: string; dimensions: number; threadId: number }): void { + const handle = this.getHandle(params.storePath, params.dimensions); + handle.db.exec(`delete from ${TABLE_NAME} where rowid = ${Math.trunc(params.threadId)}`); + } + + queryNearest(params: VectorQueryParams): VectorNeighbor[] { + const handle = this.getHandle(params.storePath, params.dimensions); + const safeLimit = Math.max(1, params.limit); + const safeCandidateK = Math.max(safeLimit, params.candidateK ?? safeLimit); + const querySql = + params.efSearch !== undefined + ? `select rowid, distance from ${TABLE_NAME} where knn_search(vec, knn_param(?, ${safeCandidateK}, ${params.efSearch}))` + : `select rowid, distance from ${TABLE_NAME} where knn_search(vec, knn_param(?, ${safeCandidateK}))`; + const rows = handle.db.prepare(querySql).all([this.vectorBuffer(params.vector)]) as Array<{ rowid: number; distance: number }>; + + return rows + .filter((row) => row.rowid !== params.excludeThreadId) + .slice(0, safeLimit) + .map((row) => ({ + threadId: row.rowid, + score: this.distanceToScore(row.distance), + })); + } + + close(): void { + for (const handle of this.handles.values()) { + handle.db.close(); + } + this.handles.clear(); + } + + private getHandle(storePath: string, dimensions: number): StoreHandle { + const existing = this.handles.get(storePath); + if (existing) { + this.ensureSchema(existing, dimensions); + return existing; + } + + const db = openDb(storePath) as SqliteWithExtension; + db.pragma('journal_mode = WAL'); + db.pragma('synchronous = NORMAL'); + db.loadExtension(this.resolveExtensionPath()); + const handle: StoreHandle = { db, storePath, dimensions: null }; + this.handles.set(storePath, handle); + this.ensureSchema(handle, dimensions); + return handle; + } + + private ensureSchema(handle: StoreHandle, dimensions: number): void { + handle.db.exec(`create table if not exists ${META_TABLE_NAME} (id integer primary key check (id = 1), dimensions integer not null)`); + const meta = handle.db.prepare(`select dimensions from ${META_TABLE_NAME} where id = 1`).get() as { dimensions: number } | undefined; + const tableExists = Boolean( + handle.db.prepare("select 1 from sqlite_master where type = 'table' and name = ? limit 1").get(TABLE_NAME), + ); + + if (!meta || meta.dimensions !== dimensions || !tableExists) { + handle.db.exec(`drop table if exists ${TABLE_NAME}`); + handle.db.exec(`delete from ${META_TABLE_NAME}`); + const indexPath = this.indexPath(handle.storePath); + handle.db.exec( + `create virtual table ${TABLE_NAME} using vectorlite(vec float32[${dimensions}], hnsw(max_elements=${HNSW_MAX_ELEMENTS}), '${this.escapeSqlString(indexPath)}')`, + ); + handle.db.prepare(`insert into ${META_TABLE_NAME}(id, dimensions) values (1, ?)`).run(dimensions); + } + + handle.dimensions = dimensions; + } + + private resolveExtensionPath(): string { + if (this.options.extensionPathProvider) { + return this.options.extensionPathProvider(); + } + const vectorlite = requireFromHere('vectorlite') as { vectorlitePath: () => string }; + return vectorlite.vectorlitePath(); + } + + private vectorBuffer(vector: number[]): Buffer { + return Buffer.from(Float32Array.from(vector).buffer); + } + + private distanceToScore(distance: number): number { + return 1 - distance / 2; + } + + private indexPath(storePath: string): string { + return path.join(path.dirname(storePath), `${path.basename(storePath, path.extname(storePath))}.hnsw`); + } + + private escapeSqlString(value: string): string { + return value.replace(/'/g, "''"); + } +} From 57b491815ff4d0559083d8a48c4b5b34142ec6a8 Mon Sep 17 00:00:00 2001 From: huntharo Date: Wed, 1 Apr 2026 19:52:39 -0400 Subject: [PATCH 27/34] feat: add vector migration operator controls --- README.md | 49 ++++++++-- apps/cli/README.md | 49 ++++++++-- apps/cli/src/main.test.ts | 40 ++++++++- apps/cli/src/main.ts | 124 +++++++++++++++++++++++++- skills/ghcrawl/SKILL.md | 15 +++- skills/ghcrawl/references/protocol.md | 16 +++- 6 files changed, 268 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index a8ad294..9788d65 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ GitHub is required to crawl issue and PR data. OpenAI is required for embeddings ```bash ghcrawl init +ghcrawl configure ghcrawl doctor ghcrawl refresh owner/repo ghcrawl tui owner/repo @@ -44,11 +45,12 @@ ghcrawl tui owner/repo - save plaintext keys in `~/.config/ghcrawl/config.json` - or guide you through a 1Password CLI (`op`) setup that keeps keys out of the config file -`ghcrawl refresh owner/repo` is the main pipeline command. It pulls the latest open GitHub issues and pull requests, refreshes embeddings for changed items, and rebuilds the clusters you browse in the TUI. +`ghcrawl refresh owner/repo` is the main pipeline command. It pulls the latest open GitHub issues and pull requests, summarizes changed items when the active embedding basis depends on summaries, refreshes vectors, and rebuilds the clusters you browse in the TUI. ## Typical Commands ```bash +ghcrawl configure ghcrawl doctor ghcrawl refresh owner/repo ghcrawl tui owner/repo @@ -56,7 +58,7 @@ ghcrawl tui owner/repo `refresh`, `sync`, and `embed` call remote services and should be run intentionally. -`cluster` does not call remote services, but it is still time consuming. On a repo with roughly `12k` issues and PRs, a full cluster rebuild can take around `10 minutes`. +`cluster` does not call remote services, but it is still time consuming. It now uses a persistent `vectorlite` index instead of exact in-memory scans, so large-repo rebuilds are materially faster, but still not instant. `clusters` explores the clusters already stored in the local SQLite database and is expected to be the fast, read-only inspection path. @@ -72,6 +74,7 @@ ghcrawl refresh --help For agent-facing and script-facing commands, prefer explicit machine mode: ```bash +ghcrawl configure --json ghcrawl doctor --json ghcrawl threads owner/repo --numbers 42,43,44 --json ghcrawl clusters owner/repo --min-size 10 --limit 20 --sort recent --json @@ -118,11 +121,12 @@ If you need tighter control, you can run the three stages yourself: ```bash ghcrawl sync owner/repo # pull the latest open issues and pull requests from GitHub -ghcrawl embed owner/repo # generate or refresh OpenAI embeddings for changed items +ghcrawl summarize owner/repo # optional explicit summary refresh when using title_summary +ghcrawl embed owner/repo # generate or refresh the single active vector per thread ghcrawl cluster owner/repo # rebuild local related-work clusters from the current vectors (local-only, but can take ~10 minutes on a ~12k issue/PR repo) ``` -Run them in that order. `refresh` is just the safe convenience command that performs the same sequence for you. +Run them in that order. If your embedding basis is `title_summary`, `refresh` automatically inserts the summarize stage before embed for you. ## Init And Doctor @@ -158,8 +162,27 @@ GitHub token guidance: - local DB path wiring - GitHub token presence, token-shape validation, and a live auth smoke check - OpenAI key presence, key-shape validation, and a live auth smoke check +- `vectorlite` runtime readiness - if init is configured for 1Password CLI but you forgot to run through your `op` wrapper, doctor tells you that explicitly +## Configure + +Use `configure` to inspect or change the active summary model and embedding basis: + +```bash +ghcrawl configure +ghcrawl configure --summary-model gpt-5.4-mini +ghcrawl configure --embedding-basis title_original +``` + +Current defaults: + +- summary model: `gpt-5-mini` +- embedding basis: `title_summary` (`title + dedupe summary`) +- vector backend: `vectorlite` + +Changing the summary model or embedding basis makes the next `refresh` rebuild vectors and clusters for that repo. + ### 1Password CLI Example If you choose 1Password CLI mode, create a 1Password Secure Note with concealed fields named exactly: @@ -214,10 +237,17 @@ Use `close-cluster` when you want to locally suppress a whole cluster from defau ## Cost To Operate -The main variable cost is OpenAI embeddings. Current model pricing is published by OpenAI here: [OpenAI API pricing](https://developers.openai.com/api/docs/pricing#embeddings). +The main variable costs are summarization and embeddings. Embedding pricing is published by OpenAI here: [OpenAI API pricing](https://developers.openai.com/api/docs/pricing#embeddings). On a real local run against roughly `12k` issues plus about `1.2x` related PR and issue inputs, [`text-embedding-3-large`](https://developers.openai.com/api/docs/pricing#embeddings) came out to about **$0.65 USD** total to embed the repo. Treat that as an approximate data point for something like `~14k` issue and PR inputs, not a hard guarantee. +For one-time summary migration planning on a repo around the size of `openclaw/openclaw` (`~20k` issues and PRs), `ghcrawl configure` reports these operator estimates using the April 1, 2026 USD pricing assumptions for this release: + +- `gpt-5-mini`: about **$12 USD** one time +- `gpt-5.4-mini`: about **$30 USD** one time + +`gpt-5-mini` is the default to keep that migration cost lower. `gpt-5.4-mini` is available when you want higher-quality summaries and are comfortable with the higher one-time spend. + This screenshot is the reference point for that estimate: ![OpenAI embeddings cost for a 12k-issue repo](./docs/images/openai-embeddings-12k-issue-repo.png) @@ -265,15 +295,16 @@ The agent and build contract for this repo lives in [SPEC.md](./SPEC.md). - a plain `sync owner/repo` is incremental by default after the first full completed open scan for that repo - `sync` is metadata-only by default - `sync --include-comments` enables issue comments, PR reviews, and review comments for deeper context -- `embed` defaults to `text-embedding-3-large` -- `embed` generates separate vectors for `title` and `body`, and also uses stored summary text when present -- `embed` stores an input hash per source kind and will not resubmit unchanged text for re-embedding +- `embed` defaults to `text-embedding-3-large` with `dimensions=1024` +- `embed` maintains one active vector per thread, stored in a persistent `vectorlite` sidecar index +- `embed` stores an input hash per thread and will not resubmit unchanged text for re-embedding +- the default embedding basis is `title + dedupe summary`; use `ghcrawl configure` to switch to `title + original body` - `sync --since` accepts ISO timestamps and relative durations like `15m`, `2h`, `7d`, and `1mo` - `sync --limit ` is the best smoke-test path on a busy repository - `tui` remembers sort order and min cluster size per repository in the persisted config file - the TUI shows locally closed threads and clusters in gray; press `x` to hide or show them - on wide screens, press `l` to toggle between three columns and a wider cluster list with members/detail stacked on the right -- if you add a brand-new repo from the TUI with `p`, ghcrawl runs sync -> embed -> cluster and opens that repo with min cluster size `1+` +- if you add a brand-new repo from the TUI with `p`, ghcrawl runs sync -> summarize-if-needed -> embed -> cluster and opens that repo with min cluster size `1+` ## Responsibility Attestation diff --git a/apps/cli/README.md b/apps/cli/README.md index 4580f5e..80dc101 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -36,6 +36,7 @@ GitHub is required to crawl issue and PR data. OpenAI is required for embeddings ```bash ghcrawl init +ghcrawl configure ghcrawl doctor ghcrawl refresh owner/repo ghcrawl tui owner/repo @@ -46,11 +47,12 @@ ghcrawl tui owner/repo - save plaintext keys in `~/.config/ghcrawl/config.json` - or guide you through a 1Password CLI (`op`) setup that keeps keys out of the config file -`ghcrawl refresh owner/repo` is the main pipeline command. It pulls the latest open GitHub issues and pull requests, refreshes embeddings for changed items, and rebuilds the clusters you browse in the TUI. +`ghcrawl refresh owner/repo` is the main pipeline command. It pulls the latest open GitHub issues and pull requests, summarizes changed items when the active embedding basis depends on summaries, refreshes vectors, and rebuilds the clusters you browse in the TUI. ## Typical Commands ```bash +ghcrawl configure ghcrawl doctor ghcrawl refresh owner/repo ghcrawl tui owner/repo @@ -58,7 +60,7 @@ ghcrawl tui owner/repo `refresh`, `sync`, and `embed` call remote services and should be run intentionally. -`cluster` does not call remote services, but it is still time consuming. On a repo with roughly `12k` issues and PRs, a full cluster rebuild can take around `10 minutes`. +`cluster` does not call remote services, but it is still time consuming. It now uses a persistent `vectorlite` index instead of exact in-memory scans, so large-repo rebuilds are materially faster, but still not instant. `clusters` explores the clusters already stored in the local SQLite database and is expected to be the fast, read-only inspection path. @@ -74,6 +76,7 @@ ghcrawl refresh --help For agent-facing and script-facing commands, prefer explicit machine mode: ```bash +ghcrawl configure --json ghcrawl doctor --json ghcrawl threads owner/repo --numbers 42,43,44 --json ghcrawl clusters owner/repo --min-size 10 --limit 20 --sort recent --json @@ -120,11 +123,12 @@ If you need tighter control, you can run the three stages yourself: ```bash ghcrawl sync owner/repo # pull the latest open issues and pull requests from GitHub -ghcrawl embed owner/repo # generate or refresh OpenAI embeddings for changed items +ghcrawl summarize owner/repo # optional explicit summary refresh when using title_summary +ghcrawl embed owner/repo # generate or refresh the single active vector per thread ghcrawl cluster owner/repo # rebuild local related-work clusters from the current vectors (local-only, but can take ~10 minutes on a ~12k issue/PR repo) ``` -Run them in that order. `refresh` is just the safe convenience command that performs the same sequence for you. +Run them in that order. If your embedding basis is `title_summary`, `refresh` automatically inserts the summarize stage before embed for you. ## Init And Doctor @@ -160,8 +164,27 @@ GitHub token guidance: - local DB path wiring - GitHub token presence, token-shape validation, and a live auth smoke check - OpenAI key presence, key-shape validation, and a live auth smoke check +- `vectorlite` runtime readiness - if init is configured for 1Password CLI but you forgot to run through your `op` wrapper, doctor tells you that explicitly +## Configure + +Use `configure` to inspect or change the active summary model and embedding basis: + +```bash +ghcrawl configure +ghcrawl configure --summary-model gpt-5.4-mini +ghcrawl configure --embedding-basis title_original +``` + +Current defaults: + +- summary model: `gpt-5-mini` +- embedding basis: `title_summary` (`title + dedupe summary`) +- vector backend: `vectorlite` + +Changing the summary model or embedding basis makes the next `refresh` rebuild vectors and clusters for that repo. + ### 1Password CLI Example If you choose 1Password CLI mode, create a 1Password Secure Note with concealed fields named exactly: @@ -216,10 +239,17 @@ Use `close-cluster` when you want to locally suppress a whole cluster from defau ## Cost To Operate -The main variable cost is OpenAI embeddings. Current model pricing is published by OpenAI here: [OpenAI API pricing](https://developers.openai.com/api/docs/pricing#embeddings). +The main variable costs are summarization and embeddings. Embedding pricing is published by OpenAI here: [OpenAI API pricing](https://developers.openai.com/api/docs/pricing#embeddings). On a real local run against roughly `12k` issues plus about `1.2x` related PR and issue inputs, [`text-embedding-3-large`](https://developers.openai.com/api/docs/pricing#embeddings) came out to about **$0.65 USD** total to embed the repo. Treat that as an approximate data point for something like `~14k` issue and PR inputs, not a hard guarantee. +For one-time summary migration planning on a repo around the size of `openclaw/openclaw` (`~20k` issues and PRs), `ghcrawl configure` reports these operator estimates using the April 1, 2026 USD pricing assumptions for this release: + +- `gpt-5-mini`: about **$12 USD** one time +- `gpt-5.4-mini`: about **$30 USD** one time + +`gpt-5-mini` is the default to keep that migration cost lower. `gpt-5.4-mini` is available when you want higher-quality summaries and are comfortable with the higher one-time spend. + This screenshot is the reference point for that estimate: ![OpenAI embeddings cost for a 12k-issue repo](https://raw.githubusercontent.com/pwrdrvr/ghcrawl/main/docs/images/openai-embeddings-12k-issue-repo.png) @@ -267,15 +297,16 @@ The agent and build contract for this repo lives in [SPEC.md](https://github.com - a plain `sync owner/repo` is incremental by default after the first full completed open scan for that repo - `sync` is metadata-only by default - `sync --include-comments` enables issue comments, PR reviews, and review comments for deeper context -- `embed` defaults to `text-embedding-3-large` -- `embed` generates separate vectors for `title` and `body`, and also uses stored summary text when present -- `embed` stores an input hash per source kind and will not resubmit unchanged text for re-embedding +- `embed` defaults to `text-embedding-3-large` with `dimensions=1024` +- `embed` maintains one active vector per thread, stored in a persistent `vectorlite` sidecar index +- `embed` stores an input hash per thread and will not resubmit unchanged text for re-embedding +- the default embedding basis is `title + dedupe summary`; use `ghcrawl configure` to switch to `title + original body` - `sync --since` accepts ISO timestamps and relative durations like `15m`, `2h`, `7d`, and `1mo` - `sync --limit ` is the best smoke-test path on a busy repository - `tui` remembers sort order and min cluster size per repository in the persisted config file - the TUI shows locally closed threads and clusters in gray; press `x` to hide or show them - on wide screens, press `l` to toggle between three columns and a wider cluster list with members/detail stacked on the right -- if you add a brand-new repo from the TUI with `p`, ghcrawl runs sync -> embed -> cluster and opens that repo with min cluster size `1+` +- if you add a brand-new repo from the TUI with `p`, ghcrawl runs sync -> summarize-if-needed -> embed -> cluster and opens that repo with min cluster size `1+` ## Responsibility Attestation diff --git a/apps/cli/src/main.test.ts b/apps/cli/src/main.test.ts index 020e06b..6e91fa1 100644 --- a/apps/cli/src/main.test.ts +++ b/apps/cli/src/main.test.ts @@ -5,7 +5,7 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { GHCrawlService } from '@ghcrawl/api-core'; +import { GHCrawlService, readPersistedConfig } from '@ghcrawl/api-core'; import { formatDoctorReport, formatLogLine, getExitCode, parseOwnerRepo, parseRepoFlags, resolveSinceValue, run, runCli } from './main.js'; function createWritableCapture(isTTY?: boolean) { @@ -39,6 +39,7 @@ function makeRunContext(): { env: NodeJS.ProcessEnv; cwd: string; cleanup: () => const publicCommands = [ 'init', 'doctor', + 'configure', 'version', 'sync', 'refresh', @@ -171,6 +172,38 @@ test('run prints json doctor output when explicitly requested', async () => { assert.match(stdout.read(), /"github"/); }); +test('configure prints current persisted settings and cost estimates', async () => { + const stdout = createWritableCapture(true); + const context = makeRunContext(); + + try { + await run(['configure'], stdout.stream, { env: context.env, cwd: context.cwd }); + } finally { + context.cleanup(); + } + + assert.match(stdout.read(), /ghcrawl configure/); + assert.match(stdout.read(), /summary model: gpt-5-mini/); + assert.match(stdout.read(), /embedding basis: title_summary/); + assert.match(stdout.read(), /gpt-5\.4-mini: ~\$30 USD/); +}); + +test('configure persists summary model changes', async () => { + const stdout = createWritableCapture(); + const context = makeRunContext(); + + try { + await run(['configure', '--summary-model', 'gpt-5.4-mini', '--json'], stdout.stream, { + env: context.env, + cwd: context.cwd, + }); + const persisted = readPersistedConfig({ env: context.env, cwd: context.cwd }); + assert.equal(persisted.data.summaryModel, 'gpt-5.4-mini'); + } finally { + context.cleanup(); + } +}); + test('unknown command exits with code 2 and a top-level help hint', async () => { const stderr = createWritableCapture(); const code = await runCli(['wat'], { stderr: stderr.stream }); @@ -410,6 +443,11 @@ test('formatDoctorReport renders a human-readable health summary', () => { authOk: false, error: 'missing', }, + vectorlite: { + configured: true, + runtimeOk: true, + error: null, + }, }); assert.match(rendered, /config path: \/tmp\/config\.json/); diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts index b00b84b..c4b0201 100644 --- a/apps/cli/src/main.ts +++ b/apps/cli/src/main.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import { parseArgs } from 'node:util'; import { fileURLToPath } from 'node:url'; -import { createApiServer, GHCrawlService, loadConfig, type LoadConfigOptions } from '@ghcrawl/api-core'; +import { createApiServer, GHCrawlService, loadConfig, readPersistedConfig, writePersistedConfig, type LoadConfigOptions } from '@ghcrawl/api-core'; import { createHeapDiagnostics, type HeapDiagnostics } from './heap-diagnostics.js'; import { runInitWizard } from './init-wizard.js'; import { startTui } from './tui/app.js'; @@ -13,6 +13,7 @@ import { startTui } from './tui/app.js'; type CommandName = | 'init' | 'doctor' + | 'configure' | 'version' | 'sync' | 'refresh' @@ -43,7 +44,28 @@ type CommandSpec = { }; type DoctorResult = Awaited>; -type DoctorReport = DoctorResult & { version: string }; +type DoctorReport = DoctorResult & { + version: string; + vectorlite?: { + configured: boolean; + runtimeOk: boolean; + error: string | null; + }; +}; + +type ConfigureReport = { + configPath: string; + updated: boolean; + summaryModel: 'gpt-5-mini' | 'gpt-5.4-mini'; + embeddingBasis: 'title_original' | 'title_summary'; + vectorBackend: 'vectorlite'; + costEstimateUsd: { + sampleThreads: number; + pricingDate: string; + gpt5Mini: number; + gpt54Mini: number; + }; +}; type ParsedGlobalFlags = { argv: string[]; @@ -80,6 +102,18 @@ const COMMAND_SPECS: readonly CommandSpec[] = [ examples: ['ghcrawl doctor', 'ghcrawl doctor --json'], agentJson: true, }, + { + name: 'configure', + synopsis: 'configure [--summary-model gpt-5-mini|gpt-5.4-mini] [--embedding-basis title_original|title_summary] [--json]', + description: 'Show or update persisted summarization and embedding settings.', + options: [ + '--summary-model Select gpt-5-mini or gpt-5.4-mini for summarization', + '--embedding-basis Select title_original or title_summary for active vectors', + '--json Emit machine-readable JSON output explicitly', + ], + examples: ['ghcrawl configure', 'ghcrawl configure --summary-model gpt-5.4-mini', 'ghcrawl configure --embedding-basis title_original --json'], + agentJson: true, + }, { name: 'version', synopsis: 'version', @@ -578,6 +612,24 @@ function parseEnum(command: CommandName, flagName: string, val throw new CliUsageError(`Invalid --${flagName}: ${value}. Use one of ${allowed.join(', ')}.`, command); } +function buildConfigureReport(options: { + configPath: string; + updated: boolean; + summaryModel: 'gpt-5-mini' | 'gpt-5.4-mini'; + embeddingBasis: 'title_original' | 'title_summary'; + vectorBackend: 'vectorlite'; +}): ConfigureReport { + return { + ...options, + costEstimateUsd: { + sampleThreads: 20_000, + pricingDate: 'April 1, 2026', + gpt5Mini: 12, + gpt54Mini: 30, + }, + }; +} + export function formatDoctorReport(result: DoctorReport): string { const lines = [ 'ghcrawl doctor', @@ -610,6 +662,39 @@ export function formatDoctorReport(result: DoctorReport): string { if (result.openai.error) { lines.push(` note: ${result.openai.error}`); } + lines.push( + '', + 'Vectorlite', + ` configured: ${formatBooleanStatus(result.vectorlite?.configured ?? false)}`, + ` runtime ok: ${formatBooleanStatus(result.vectorlite?.runtimeOk ?? false)}`, + ); + if (result.vectorlite?.error) { + lines.push(` note: ${result.vectorlite.error}`); + } + return `${lines.join('\n')}\n`; +} + +export function formatConfigureReport(result: ConfigureReport): string { + const basisLabel = result.embeddingBasis === 'title_summary' + ? 'title + dedupe summary' + : 'title + original body'; + const lines = [ + 'ghcrawl configure', + `config path: ${result.configPath}`, + `updated: ${result.updated ? 'yes' : 'no'}`, + '', + 'Active settings', + ` summary model: ${result.summaryModel}`, + ` embedding basis: ${result.embeddingBasis} (${basisLabel})`, + ` vector backend: ${result.vectorBackend}`, + '', + `Estimated one-time summary cost for ~${result.costEstimateUsd.sampleThreads.toLocaleString()} threads`, + ` pricing date: ${result.costEstimateUsd.pricingDate}`, + ` gpt-5-mini: ~$${result.costEstimateUsd.gpt5Mini.toFixed(0)} USD`, + ` gpt-5.4-mini: ~$${result.costEstimateUsd.gpt54Mini.toFixed(0)} USD`, + '', + 'Changing summary model or embedding basis will make the next refresh rebuild vectors and clusters.', + ]; return `${lines.join('\n')}\n`; } @@ -785,6 +870,41 @@ export async function run( stdout.write(shouldWriteJson ? `${JSON.stringify(result, null, 2)}\n` : formatDoctorReport(result)); return; } + case 'configure': { + const parsed = parseArgsForCommand('configure', rest, { + 'summary-model': { type: 'string' }, + 'embedding-basis': { type: 'string' }, + json: { type: 'boolean' }, + }); + const values = parsed.values as RepoCommandValues; + const summaryModel = parseEnum('configure', 'summary-model', values['summary-model'], ['gpt-5-mini', 'gpt-5.4-mini']); + const embeddingBasis = parseEnum('configure', 'embedding-basis', values['embedding-basis'], ['title_original', 'title_summary']); + const current = getConfig(); + const stored = readPersistedConfig(loadConfigOptions); + const next = { + ...stored.data, + summaryModel: summaryModel ?? current.summaryModel, + embeddingBasis: embeddingBasis ?? current.embeddingBasis, + vectorBackend: 'vectorlite' as const, + }; + const updated = + next.summaryModel !== current.summaryModel || + next.embeddingBasis !== current.embeddingBasis || + next.vectorBackend !== current.vectorBackend; + if (updated) { + writePersistedConfig(next, loadConfigOptions); + } + const result = buildConfigureReport({ + configPath: current.configPath, + updated, + summaryModel: next.summaryModel as 'gpt-5-mini' | 'gpt-5.4-mini', + embeddingBasis: next.embeddingBasis as 'title_original' | 'title_summary', + vectorBackend: 'vectorlite', + }); + const shouldWriteJson = values.json === true || (stdout as NodeJS.WriteStream).isTTY !== true; + stdout.write(shouldWriteJson ? `${JSON.stringify(result, null, 2)}\n` : formatConfigureReport(result)); + return; + } case 'version': { stdout.write(`${CLI_VERSION}\n`); return; diff --git a/skills/ghcrawl/SKILL.md b/skills/ghcrawl/SKILL.md index 58526f4..375c598 100644 --- a/skills/ghcrawl/SKILL.md +++ b/skills/ghcrawl/SKILL.md @@ -21,6 +21,13 @@ In default mode, do not treat missing credentials as a problem unless the user e Even in API-enabled mode, never run `sync`, `embed`, `cluster`, or `refresh` unless the user explicitly asks for that work. Those commands can take a long time, consume paid API usage, and trigger rate limiting if used too often. +Current pipeline defaults to keep in mind: + +- persistent semantic search and clustering use a `vectorlite` sidecar index +- the default summary model is `gpt-5-mini` +- the default embedding basis is `title_summary`, which means `refresh` may run summarize before embed +- changing summary model or embedding basis with `ghcrawl configure` makes the next refresh rebuild vectors and clusters + Also never run `close-thread` or `close-cluster` unless the user explicitly asks you to mark a local thread or cluster closed. ## When to use this skill @@ -59,6 +66,7 @@ ghcrawl threads owner/repo --numbers 42,43,44 --json ghcrawl author owner/repo --login lqquan --json ghcrawl search owner/repo --query "download stalls" --mode hybrid --json ghcrawl neighbors owner/repo --number 42 --limit 10 --json +ghcrawl configure --json ``` These operate on the existing local SQLite dataset. @@ -74,6 +82,8 @@ If the user explicitly wants to inspect those records, add `--include-closed`. Use `threads --numbers 12345` when you need to find the cluster for one specific issue/PR number. The returned thread record includes `clusterId`. If it is non-null, follow with `cluster-detail --id `. +Use `configure --json` when you need to confirm the currently selected summary model or embedding basis before suggesting an expensive refresh. + Use `threads --numbers ...` when you need a batch of specific issue/PR records. Do not pay the CLI startup cost 10 times for 10 separate single-thread lookups. Use `author --login ...` when you need one author's open threads and their strongest stored same-author similarity matches in one call. @@ -134,8 +144,9 @@ ghcrawl refresh owner/repo This runs, in fixed order: 1. GitHub sync/reconcile -2. embed refresh -3. cluster rebuild +2. summarize-if-needed +3. embed refresh +4. cluster rebuild You may skip steps only when the user explicitly wants that or the freshness state makes it unnecessary: diff --git a/skills/ghcrawl/references/protocol.md b/skills/ghcrawl/references/protocol.md index 9310032..582a4f4 100644 --- a/skills/ghcrawl/references/protocol.md +++ b/skills/ghcrawl/references/protocol.md @@ -24,6 +24,16 @@ Do not call this automatically on every skill invocation. Use it when: If the user asked only for read-only analysis, missing auth is not itself a blocker. Work from the existing local dataset through the CLI. +### `ghcrawl configure --json` + +Shows the current persisted summary model, embedding basis, vector backend, and the built-in one-time summary cost estimate. + +Use this when: + +- you need to confirm whether summaries are using `gpt-5-mini` or `gpt-5.4-mini` +- you need to confirm whether embeddings are built from `title_original` or `title_summary` +- you want to estimate whether a first refresh after a config change will be expensive + ### `ghcrawl threads owner/repo --numbers --json` Bulk read path for specific issue/PR numbers from the local DB. @@ -63,8 +73,9 @@ Useful flags: Runs the staged pipeline in fixed order: 1. GitHub sync/reconcile -2. embeddings -3. clusters +2. summarize-if-needed +3. embeddings +4. clusters Optional skips: @@ -173,6 +184,7 @@ If `ghcrawl` is not installed globally: ```bash pnpm --filter ghcrawl cli doctor --json +pnpm --filter ghcrawl cli configure --json pnpm --filter ghcrawl cli threads owner/repo --numbers 12345 --json pnpm --filter ghcrawl cli threads owner/repo --numbers 42,43,44 --json pnpm --filter ghcrawl cli threads owner/repo --numbers 42,43,44 --include-closed --json From 2e334ce59abda51d4f9608e6a7c399d4aad1a271 Mon Sep 17 00:00:00 2001 From: huntharo Date: Thu, 2 Apr 2026 08:55:49 -0400 Subject: [PATCH 28/34] fix: compact migrated vectors and tune cluster recall --- packages/api-core/src/service.test.ts | 146 +++++++++++++++++++++++++- packages/api-core/src/service.ts | 144 ++++++++++++++++++++++--- 2 files changed, 274 insertions(+), 16 deletions(-) diff --git a/packages/api-core/src/service.test.ts b/packages/api-core/src/service.test.ts index f87989b..48d96b6 100644 --- a/packages/api-core/src/service.test.ts +++ b/packages/api-core/src/service.test.ts @@ -777,10 +777,13 @@ test('embedRepository batches multi-source embeddings and skips unchanged inputs assert.equal(embedCalls.length, 1); assert.deepEqual( service.db - .prepare('select basis from thread_vectors order by basis asc') + .prepare('select basis, vector_json from thread_vectors order by basis asc') .all() - .map((row: unknown) => (row as { basis: string }).basis), - ['title_original'], + .map((row: unknown) => { + const typed = row as { basis: string; vector_json: Buffer | string }; + return { basis: typed.basis, vectorKind: Buffer.isBuffer(typed.vector_json) ? 'blob' : typeof typed.vector_json }; + }), + [{ basis: 'title_original', vectorKind: 'blob' }], ); const second = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); @@ -799,6 +802,66 @@ test('embedRepository batches multi-source embeddings and skips unchanged inputs } }); +test('listNeighbors uses the vectorlite sidecar for current active vectors', async () => { + const service = new GHCrawlService({ + config: makeTestConfig(), + github: { + checkAuth: async () => undefined, + getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }), + listRepositoryIssues: async () => [], + getIssue: async () => { + throw new Error('not expected'); + }, + getPull: async () => { + throw new Error('not expected'); + }, + listIssueComments: async () => [], + listPullReviews: async () => [], + listPullReviewComments: async () => [], + }, + ai: { + checkAuth: async () => undefined, + summarizeThread: async () => { + throw new Error('not expected'); + }, + embedTexts: async ({ texts }) => texts.map((_text, index) => (index === 0 ? makeEmbedding(1, 0) : makeEmbedding(0.99, 0.01))), + }, + }); + + try { + const now = '2026-03-09T00:00:00Z'; + const insertThread = service.db.prepare( + `insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at) + values (?, ?, ?, ?, ?, ?, ?)`, + ); + insertThread.run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now); + const insert = service.db.prepare( + `insert into threads ( + id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url, + labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh, + merged_at_gh, first_pulled_at, last_pulled_at, updated_at + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ); + insert.run(10, 1, '100', 42, 'issue', 'open', 'Downloader hangs', 'The transfer never finishes.', 'alice', 'User', 'https://github.com/openclaw/openclaw/issues/42', '[]', '[]', '{}', 'hash-42', 0, now, now, null, null, now, now, now); + insert.run(11, 1, '101', 43, 'issue', 'open', 'Downloader retry issue', 'The transfer retries forever.', 'bob', 'User', 'https://github.com/openclaw/openclaw/issues/43', '[]', '[]', '{}', 'hash-43', 0, now, now, null, null, now, now, now); + + await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); + + const result = service.listNeighbors({ + owner: 'openclaw', + repo: 'openclaw', + threadNumber: 42, + limit: 2, + minScore: 0.1, + }); + + assert.equal(result.thread.number, 42); + assert.deepEqual(result.neighbors.map((neighbor) => neighbor.number), [43]); + } finally { + service.close(); + } +}); + test('embedRepository truncates oversized inputs before submission', async () => { const embedCalls: string[][] = []; const service = new GHCrawlService({ @@ -1519,6 +1582,83 @@ test('clusterRepository prunes older cluster runs for the repo after a successfu } }); +test('clusterRepository purges legacy embeddings and inline vector payloads after a current-vector rebuild', async () => { + const service = new GHCrawlService({ + config: makeTestConfig(), + github: { + checkAuth: async () => undefined, + getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }), + listRepositoryIssues: async () => [], + getIssue: async () => { + throw new Error('not expected'); + }, + getPull: async () => { + throw new Error('not expected'); + }, + listIssueComments: async () => [], + listPullReviews: async () => [], + listPullReviewComments: async () => [], + }, + ai: { + checkAuth: async () => undefined, + summarizeThread: async () => { + throw new Error('not expected'); + }, + embedTexts: async ({ texts }) => texts.map((_text, index) => (index === 0 ? makeEmbedding(1, 0) : makeEmbedding(0.99, 0.01))), + }, + }); + + try { + const now = '2026-03-09T00:00:00Z'; + service.db + .prepare( + `insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at) + values (?, ?, ?, ?, ?, ?, ?)`, + ) + .run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now); + + const insertThread = service.db.prepare( + `insert into threads ( + id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url, + labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh, + merged_at_gh, first_pulled_at, last_pulled_at, updated_at + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ); + insertThread.run(10, 1, '100', 42, 'issue', 'open', 'Downloader hangs', 'The transfer never finishes.', 'alice', 'User', 'https://github.com/openclaw/openclaw/issues/42', '[]', '[]', '{}', 'hash-42', 0, now, now, null, null, now, now, now); + insertThread.run(11, 1, '101', 43, 'issue', 'open', 'Fix downloader hang', 'Implements a fix.', 'bob', 'User', 'https://github.com/openclaw/openclaw/issues/43', '[]', '[]', '{}', 'hash-43', 0, now, now, null, null, now, now, now); + + const insertLegacy = service.db.prepare( + `insert into document_embeddings (thread_id, source_kind, model, dimensions, content_hash, embedding_json, created_at, updated_at) + values (?, ?, ?, ?, ?, ?, ?, ?)`, + ); + for (const sourceKind of ['title', 'body', 'dedupe_summary'] as const) { + insertLegacy.run(10, sourceKind, 'text-embedding-3-large', 2, `hash-42-${sourceKind}`, '[1,0]', now, now); + insertLegacy.run(11, sourceKind, 'text-embedding-3-large', 2, `hash-43-${sourceKind}`, '[0.99,0.01]', now, now); + } + + await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); + const beforeCluster = service.db.prepare('select count(*) as count from document_embeddings').get() as { count: number }; + assert.equal(beforeCluster.count, 6); + + await service.clusterRepository({ + owner: 'openclaw', + repo: 'openclaw', + k: 1, + minScore: 0.5, + }); + + const legacyCount = service.db.prepare('select count(*) as count from document_embeddings').get() as { count: number }; + const inlineVectors = service.db + .prepare('select typeof(vector_json) as vector_kind from thread_vectors order by thread_id asc') + .all() as Array<{ vector_kind: string }>; + + assert.equal(legacyCount.count, 0); + assert.deepEqual(inlineVectors.map((row) => row.vector_kind), ['blob', 'blob']); + } finally { + service.close(); + } +}); + test('clusterRepository does not retain a parsed embedding cache in-process', async () => { const service = makeTestService({ checkAuth: async () => undefined, diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index a36958e..ff79e15 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -135,7 +135,7 @@ type ActiveVectorRow = ThreadRow & { model: string; dimensions: number; content_hash: string; - vector_json: string; + vector_json: Buffer | string; vector_backend: string; }; @@ -362,6 +362,11 @@ const EMBED_CONTEXT_RETRY_TARGET_BUFFER_RATIO = 0.95; const SUMMARY_PROMPT_VERSION = 'v1'; const ACTIVE_EMBED_DIMENSIONS = 1024; const ACTIVE_EMBED_PIPELINE_VERSION = 'vectorlite-1024-v1'; +const DEFAULT_CLUSTER_MIN_SCORE = 0.78; +const VECTORLITE_CLUSTER_EXPANDED_K = 24; +const VECTORLITE_CLUSTER_EXPANDED_MULTIPLIER = 4; +const VECTORLITE_CLUSTER_EXPANDED_CANDIDATE_K = 512; +const VECTORLITE_CLUSTER_EXPANDED_EF_SEARCH = 1024; function nowIso(): string { return new Date().toISOString(); @@ -1259,7 +1264,7 @@ export class GHCrawlService { }): Promise { const repository = this.requireRepository(params.owner, params.repo); const runId = this.startRun('cluster_runs', repository.id, repository.fullName); - const minScore = params.minScore ?? 0.82; + const minScore = params.minScore ?? DEFAULT_CLUSTER_MIN_SCORE; const k = params.k ?? 6; try { @@ -1267,23 +1272,24 @@ export class GHCrawlService { let aggregatedEdges: Map }>; if (this.isRepoVectorStateCurrent(repository.id)) { - const vectorItems = this.loadClusterableActiveVectorMeta(repository.id); + const vectorItems = this.loadClusterableActiveVectorMeta(repository.id, repository.fullName); const activeIds = new Set(vectorItems.map((item) => item.id)); - const candidateK = Math.max(k * 16, 64); + const annQuery = this.getVectorliteClusterQuery(vectorItems.length, k); aggregatedEdges = new Map(); let processed = 0; let lastProgressAt = Date.now(); params.onProgress?.( - `[cluster] loaded ${vectorItems.length} active vector(s) for ${repository.fullName} backend=${this.config.vectorBackend} k=${k} candidateK=${candidateK} minScore=${minScore}`, + `[cluster] loaded ${vectorItems.length} active vector(s) for ${repository.fullName} backend=${this.config.vectorBackend} k=${k} query_limit=${annQuery.limit} candidateK=${annQuery.candidateK} efSearch=${annQuery.efSearch ?? 'default'} minScore=${minScore}`, ); for (const item of vectorItems) { const neighbors = this.vectorStore.queryNearest({ storePath: this.repoVectorStorePath(repository.fullName), dimensions: ACTIVE_EMBED_DIMENSIONS, vector: item.embedding, - limit: k, - candidateK: candidateK + 1, + limit: annQuery.limit, + candidateK: annQuery.candidateK + 1, + efSearch: annQuery.efSearch, excludeThreadId: item.id, }); for (const neighbor of neighbors) { @@ -1341,6 +1347,7 @@ export class GHCrawlService { this.pruneOldClusterRuns(repository.id, runId); if (this.isRepoVectorStateCurrent(repository.id)) { this.markRepoClustersCurrent(repository.id); + this.cleanupMigratedRepositoryArtifacts(repository.id, repository.fullName, params.onProgress); } params.onProgress?.(`[cluster] persisted ${clusters.length} cluster(s) and pruned older cluster runs`); @@ -1376,7 +1383,7 @@ export class GHCrawlService { const sourceKinds = params.sourceKinds ?? loaded.sourceKinds; const items = loaded.items; const aggregation = params.aggregation ?? 'max'; - const minScore = params.minScore ?? 0.82; + const minScore = params.minScore ?? DEFAULT_CLUSTER_MIN_SCORE; const k = params.k ?? 6; const candidateK = Math.max(k, params.candidateK ?? Math.max(k * 16, 64)); const efSearch = params.efSearch; @@ -1794,7 +1801,7 @@ export class GHCrawlService { .queryNearest({ storePath: this.repoVectorStorePath(repository.fullName), dimensions: ACTIVE_EMBED_DIMENSIONS, - vector: JSON.parse(targetRow.vector_json) as number[], + vector: this.parseStoredVector(targetRow.vector_json), limit: limit * 2, candidateK: Math.max(limit * 8, 64), excludeThreadId: targetRow.id, @@ -2589,6 +2596,51 @@ export class GHCrawlService { }); } + private cleanupMigratedRepositoryArtifacts(repoId: number, repoFullName: string, onProgress?: (message: string) => void): void { + const legacyEmbeddingCount = this.countLegacyEmbeddings(repoId); + const inlineJsonVectorCount = this.countInlineJsonThreadVectors(repoId); + if (legacyEmbeddingCount === 0 && inlineJsonVectorCount === 0) { + return; + } + + if (legacyEmbeddingCount > 0) { + this.db + .prepare( + `delete from document_embeddings + where thread_id in (select id from threads where repo_id = ?)`, + ) + .run(repoId); + onProgress?.(`[cleanup] removed ${legacyEmbeddingCount} legacy document embedding row(s) after vector migration`); + } + + if (inlineJsonVectorCount > 0) { + const rows = this.db + .prepare( + `select tv.thread_id, tv.vector_json + from thread_vectors tv + join threads t on t.id = tv.thread_id + where t.repo_id = ? + and typeof(tv.vector_json) = 'text' + and tv.vector_json != ''`, + ) + .all(repoId) as Array<{ thread_id: number; vector_json: string }>; + const update = this.db.prepare('update thread_vectors set vector_json = ?, updated_at = ? where thread_id = ?'); + this.db.transaction(() => { + for (const row of rows) { + update.run(this.vectorBlob(JSON.parse(row.vector_json) as number[]), nowIso(), row.thread_id); + } + })(); + onProgress?.(`[cleanup] compacted ${inlineJsonVectorCount} inline SQLite vector payload(s) from JSON to binary blobs`); + } + + if (this.config.dbPath !== ':memory:') { + onProgress?.(`[cleanup] checkpointing WAL and vacuuming ${repoFullName} migration changes`); + this.db.pragma('wal_checkpoint(TRUNCATE)'); + this.db.exec('VACUUM'); + this.db.pragma('wal_checkpoint(TRUNCATE)'); + } + } + private getLatestClusterRun(repoId: number): { id: number; finished_at: string | null } | null { const state = this.getRepoPipelineState(repoId); if (state && !this.isRepoClusterStateCurrent(repoId)) { @@ -3736,7 +3788,7 @@ export class GHCrawlService { }; } - private loadClusterableActiveVectorMeta(repoId: number): Array<{ id: number; number: number; title: string; embedding: number[] }> { + private loadClusterableActiveVectorMeta(repoId: number, _repoFullName: string): Array<{ id: number; number: number; title: string; embedding: number[] }> { const rows = this.db .prepare( `select t.id, t.number, t.title, tv.vector_json @@ -3754,13 +3806,13 @@ export class GHCrawlService { id: number; number: number; title: string; - vector_json: string; + vector_json: Buffer | string; }>; return rows.map((row) => ({ id: row.id, number: row.number, title: row.title, - embedding: JSON.parse(row.vector_json) as number[], + embedding: this.parseStoredVector(row.vector_json), })); } @@ -4272,7 +4324,7 @@ export class GHCrawlService { this.config.embedModel, embedding.length, contentHash, - asJson(embedding), + this.vectorBlob(embedding), this.config.vectorBackend, nowIso(), nowIso(), @@ -4285,6 +4337,72 @@ export class GHCrawlService { }); } + private countLegacyEmbeddings(repoId: number): number { + const row = this.db + .prepare( + `select count(*) as count + from document_embeddings + where thread_id in (select id from threads where repo_id = ?)`, + ) + .get(repoId) as { count: number }; + return row.count; + } + + private countInlineJsonThreadVectors(repoId: number): number { + const row = this.db + .prepare( + `select count(*) as count + from thread_vectors + where thread_id in (select id from threads where repo_id = ?) + and typeof(vector_json) = 'text' + and vector_json != ''`, + ) + .get(repoId) as { count: number }; + return row.count; + } + + private getVectorliteClusterQuery(totalItems: number, requestedK: number): { + limit: number; + candidateK: number; + efSearch?: number; + } { + if (totalItems < CLUSTER_PARALLEL_MIN_EMBEDDINGS) { + return { + limit: requestedK, + candidateK: Math.max(requestedK * 16, 64), + }; + } + + const limit = Math.min( + Math.max(requestedK * VECTORLITE_CLUSTER_EXPANDED_MULTIPLIER, VECTORLITE_CLUSTER_EXPANDED_K), + Math.max(1, totalItems - 1), + ); + const candidateK = Math.min( + Math.max(limit * 16, VECTORLITE_CLUSTER_EXPANDED_CANDIDATE_K), + Math.max(limit, totalItems - 1), + ); + return { + limit, + candidateK, + efSearch: Math.max(candidateK * 2, VECTORLITE_CLUSTER_EXPANDED_EF_SEARCH), + }; + } + + private vectorBlob(values: number[]): Buffer { + return Buffer.from(Float32Array.from(values).buffer); + } + + private parseStoredVector(value: Buffer | string): number[] { + if (typeof value === 'string') { + if (!value) { + throw new Error('Stored vector payload is empty. Run refresh or embed first.'); + } + return JSON.parse(value) as number[]; + } + const floats = new Float32Array(value.buffer, value.byteOffset, Math.floor(value.byteLength / Float32Array.BYTES_PER_ELEMENT)); + return Array.from(floats); + } + private upsertEmbedding(threadId: number, sourceKind: EmbeddingSourceKind, contentHash: string, embedding: number[]): void { this.db .prepare( From 3a9db811d9407f6249f116885de32be4045f8ffb Mon Sep 17 00:00:00 2001 From: huntharo Date: Thu, 2 Apr 2026 10:11:37 -0400 Subject: [PATCH 29/34] fix: promote llm summary in tui detail pane --- apps/cli/src/tui/app.test.ts | 2 ++ apps/cli/src/tui/app.ts | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/cli/src/tui/app.test.ts b/apps/cli/src/tui/app.test.ts index 86ff761..4fdaa4d 100644 --- a/apps/cli/src/tui/app.test.ts +++ b/apps/cli/src/tui/app.test.ts @@ -73,9 +73,11 @@ test('renderDetailPane escapes user-provided text before rendering into a tags-e const rendered = renderDetailPane(detail, cluster, 'detail'); assert.match(rendered, /Cluster 1 \(#42 representative issue\)/); assert.match(rendered, /Bad \\{bold\\}title\\{\/bold\\}/); + assert.match(rendered, /LLM Summary:/); assert.match(rendered, /Body with \\{red-fg\\}tags\\{\/red-fg\\}/); assert.match(rendered, /Summary \\{yellow-fg\\}text\\{\/yellow-fg\\}/); assert.match(rendered, /Neighbor \\{blue-fg\\}title\\{\/blue-fg\\}/); + assert.ok(rendered.indexOf('LLM Summary:') < rendered.indexOf('{bold}Body{/bold}')); }); test('parseOwnerRepoValue accepts owner slash repo values and rejects invalid ones', () => { diff --git a/apps/cli/src/tui/app.ts b/apps/cli/src/tui/app.ts index 0fd8226..e354baa 100644 --- a/apps/cli/src/tui/app.ts +++ b/apps/cli/src/tui/app.ts @@ -1241,7 +1241,10 @@ export function renderDetailPane( ? `{bold}Closed:{/bold} ${escapeBlessedText(thread.closedAtLocal ?? thread.closedAtGh ?? 'yes')} ${thread.closeReasonLocal ? `(${escapeBlessedText(thread.closeReasonLocal)})` : ''}`.trimEnd() : '{bold}Closed:{/bold} no'; const summaries = Object.entries(threadDetail.summaries) - .map(([key, value]) => `{bold}${key}:{/bold}\n${escapeBlessedText(value)}`) + .map(([key, value]) => { + const label = key === 'dedupe_summary' ? 'LLM Summary' : key; + return `{bold}${escapeBlessedText(label)}:{/bold}\n${escapeBlessedText(value)}`; + }) .join('\n\n'); const neighbors = threadDetail.neighbors.length > 0 @@ -1261,10 +1264,10 @@ export function renderDetailPane( `{bold}Updated:{/bold} ${thread.updatedAtGh ?? 'unknown'}`, `{bold}Labels:{/bold} ${labels}`, `{bold}URL:{/bold} ${escapeBlessedText(thread.htmlUrl)}`, + summaries ? `\n\n${summaries}` : '', '', `{bold}Body{/bold}`, escapeBlessedText(thread.body ?? '(no body)'), - summaries ? `\n\n${summaries}` : '', `\n\n{bold}Neighbors{/bold}\n${neighbors}`, ] .filter(Boolean) From 85909d23b252061549de2399b803e4c4d1bb433b Mon Sep 17 00:00:00 2001 From: huntharo Date: Fri, 3 Apr 2026 14:20:05 -0400 Subject: [PATCH 30/34] feat: default vector refreshes to original issue text --- README.md | 10 ++++---- apps/cli/src/main.test.ts | 2 +- apps/cli/src/main.ts | 4 ++++ apps/cli/src/tui/app.test.ts | 12 ++++++++++ apps/cli/src/tui/app.ts | 34 ++++++++++++++++++++++------ packages/api-core/src/config.test.ts | 2 +- packages/api-core/src/config.ts | 4 ++-- skills/ghcrawl/SKILL.md | 3 ++- 8 files changed, 55 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9788d65..498bd86 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ ghcrawl tui owner/repo - save plaintext keys in `~/.config/ghcrawl/config.json` - or guide you through a 1Password CLI (`op`) setup that keeps keys out of the config file -`ghcrawl refresh owner/repo` is the main pipeline command. It pulls the latest open GitHub issues and pull requests, summarizes changed items when the active embedding basis depends on summaries, refreshes vectors, and rebuilds the clusters you browse in the TUI. +`ghcrawl refresh owner/repo` is the main pipeline command. It pulls the latest open GitHub issues and pull requests, summarizes changed items only when the active embedding basis depends on summaries, refreshes vectors, and rebuilds the clusters you browse in the TUI. ## Typical Commands @@ -126,7 +126,7 @@ ghcrawl embed owner/repo # generate or refresh the single active vector per t ghcrawl cluster owner/repo # rebuild local related-work clusters from the current vectors (local-only, but can take ~10 minutes on a ~12k issue/PR repo) ``` -Run them in that order. If your embedding basis is `title_summary`, `refresh` automatically inserts the summarize stage before embed for you. +Run them in that order. If your embedding basis is `title_summary`, `refresh` automatically inserts the summarize stage before embed for you. With the default `title_original` basis, `refresh` does not summarize unless you run `summarize` explicitly. ## Init And Doctor @@ -178,11 +178,13 @@ ghcrawl configure --embedding-basis title_original Current defaults: - summary model: `gpt-5-mini` -- embedding basis: `title_summary` (`title + dedupe summary`) +- embedding basis: `title_original` (`title + original body`) - vector backend: `vectorlite` Changing the summary model or embedding basis makes the next `refresh` rebuild vectors and clusters for that repo. +If you opt into `title_summary`, ghcrawl summarizes before embedding and uses `title + dedupe summary` as the active vector text. On `openclaw/openclaw`, that improved non-solo cluster membership by about 50% versus `title_original`, but it adds OpenAI spend. A first summarize of roughly `18k` open issues and PRs in that repo typically costs about `$15-$30` with `gpt-5-mini`; later refreshes are usually much cheaper because only changed items need summaries. + ### 1Password CLI Example If you choose 1Password CLI mode, create a 1Password Secure Note with concealed fields named exactly: @@ -298,7 +300,7 @@ The agent and build contract for this repo lives in [SPEC.md](./SPEC.md). - `embed` defaults to `text-embedding-3-large` with `dimensions=1024` - `embed` maintains one active vector per thread, stored in a persistent `vectorlite` sidecar index - `embed` stores an input hash per thread and will not resubmit unchanged text for re-embedding -- the default embedding basis is `title + dedupe summary`; use `ghcrawl configure` to switch to `title + original body` +- the default embedding basis is `title + original body`; use `ghcrawl configure --embedding-basis title_summary` if you want to summarize before embedding - `sync --since` accepts ISO timestamps and relative durations like `15m`, `2h`, `7d`, and `1mo` - `sync --limit ` is the best smoke-test path on a busy repository - `tui` remembers sort order and min cluster size per repository in the persisted config file diff --git a/apps/cli/src/main.test.ts b/apps/cli/src/main.test.ts index 6e91fa1..c018b8d 100644 --- a/apps/cli/src/main.test.ts +++ b/apps/cli/src/main.test.ts @@ -184,7 +184,7 @@ test('configure prints current persisted settings and cost estimates', async () assert.match(stdout.read(), /ghcrawl configure/); assert.match(stdout.read(), /summary model: gpt-5-mini/); - assert.match(stdout.read(), /embedding basis: title_summary/); + assert.match(stdout.read(), /embedding basis: title_original/); assert.match(stdout.read(), /gpt-5\.4-mini: ~\$30 USD/); }); diff --git a/apps/cli/src/main.ts b/apps/cli/src/main.ts index c4b0201..4446002 100644 --- a/apps/cli/src/main.ts +++ b/apps/cli/src/main.ts @@ -678,6 +678,9 @@ export function formatConfigureReport(result: ConfigureReport): string { const basisLabel = result.embeddingBasis === 'title_summary' ? 'title + dedupe summary' : 'title + original body'; + const summaryModeNote = result.embeddingBasis === 'title_summary' + ? 'enabled automatically during refresh' + : 'disabled by default; enable title_summary to summarize before embedding'; const lines = [ 'ghcrawl configure', `config path: ${result.configPath}`, @@ -686,6 +689,7 @@ export function formatConfigureReport(result: ConfigureReport): string { 'Active settings', ` summary model: ${result.summaryModel}`, ` embedding basis: ${result.embeddingBasis} (${basisLabel})`, + ` llm summaries: ${summaryModeNote}`, ` vector backend: ${result.vectorBackend}`, '', `Estimated one-time summary cost for ~${result.costEstimateUsd.sampleThreads.toLocaleString()} threads`, diff --git a/apps/cli/src/tui/app.test.ts b/apps/cli/src/tui/app.test.ts index 4fdaa4d..19289d9 100644 --- a/apps/cli/src/tui/app.test.ts +++ b/apps/cli/src/tui/app.test.ts @@ -6,6 +6,7 @@ import type { TuiClusterDetail, TuiRepoStats, TuiThreadDetail } from '@ghcrawl/a import { buildRefreshCliArgs, buildHelpContent, + buildUpdatePipelineHelpContent, buildUpdatePipelineLabels, describeUpdateTask, escapeBlessedText, @@ -201,6 +202,17 @@ test('buildHelpContent includes the full key command list', () => { assert.match(content, /This popup scrolls\./); }); +test('buildUpdatePipelineHelpContent explains the LLM summary tradeoff for both modes', () => { + const disabled = buildUpdatePipelineHelpContent('title_original'); + assert.match(disabled, /LLM summaries: disabled/); + assert.match(disabled, /configure --embedding-basis title_summary/); + assert.match(disabled, /\$15-\$30/); + + const enabled = buildUpdatePipelineHelpContent('title_summary'); + assert.match(enabled, /LLM summaries: enabled/); + assert.match(enabled, /about 50%/); +}); + test('buildRefreshCliArgs maps the staged selection to refresh skip flags', () => { assert.deepEqual(buildRefreshCliArgs({ owner: 'openclaw', repo: 'openclaw' }, { sync: true, embed: true, cluster: true }), [ 'refresh', diff --git a/apps/cli/src/tui/app.ts b/apps/cli/src/tui/app.ts index e354baa..d0c5920 100644 --- a/apps/cli/src/tui/app.ts +++ b/apps/cli/src/tui/app.ts @@ -836,7 +836,11 @@ export async function startTui(params: StartTuiParams): Promise { void (async () => { modalOpen = true; try { - const selection = await promptUpdatePipelineSelection(widgets.screen, snapshot?.stats ?? null); + const selection = await promptUpdatePipelineSelection( + widgets.screen, + snapshot?.stats ?? null, + params.service.config.embeddingBasis, + ); if (!selection) { render(); return; @@ -1351,6 +1355,23 @@ export function buildUpdatePipelineLabels( }); } +export function buildUpdatePipelineHelpContent(embeddingBasis: 'title_original' | 'title_summary'): string { + const summariesEnabled = embeddingBasis === 'title_summary'; + const summaryStatus = summariesEnabled + ? 'LLM summaries: enabled via title_summary.' + : 'LLM summaries: disabled; current basis is title_original.'; + const summaryAction = summariesEnabled + ? 'On openclaw/openclaw this improved non-solo cluster membership by about 50% versus title_original.' + : 'Enable with `ghcrawl configure --embedding-basis title_summary` if you want richer clustering; on openclaw/openclaw that improved non-solo cluster membership by about 50%.'; + return [ + 'Usually you want all three. Run order is fixed: GitHub sync/reconcile -> embeddings -> clusters.', + `${summaryStatus} ${summaryAction}`, + 'A first summarize of ~18k open issues/PRs in openclaw/openclaw typically costs about $15-$30 with gpt-5-mini.', + 'Later refreshes are usually much cheaper because only changed items need summaries.', + 'Toggle with space, move with arrows, Enter to start, Esc to cancel.', + ].join('\n'); +} + export function buildHelpContent(): string { return [ '{bold}ghcrawl TUI Help{/bold}', @@ -1477,6 +1498,7 @@ async function promptHelp(screen: blessed.Widgets.Screen): Promise { async function promptUpdatePipelineSelection( screen: blessed.Widgets.Screen, stats: TuiRepoStats | null, + embeddingBasis: 'title_original' | 'title_summary', ): Promise { const selection: UpdateTaskSelection = { sync: true, embed: true, cluster: true }; const modalWidth = '76%'; @@ -1490,7 +1512,7 @@ async function promptUpdatePipelineSelection( top: 'center', left: 'center', width: modalWidth, - height: 11, + height: 14, style: { border: { fg: '#5bc0eb' }, item: { fg: 'white' }, @@ -1500,14 +1522,12 @@ async function promptUpdatePipelineSelection( }); const help = blessed.box({ parent: screen, - top: 'center-4', + top: 'center-5', left: 'center', width: modalWidth, - height: 4, + height: 7, style: { fg: 'white', bg: '#101522' }, - content: - 'Usually you want all three. Run order is fixed: GitHub sync/reconcile -> embeddings -> clusters.\n' + - 'Toggle with space, move with arrows, Enter to start, Esc to cancel.', + content: buildUpdatePipelineHelpContent(embeddingBasis), }); box.focus(); diff --git a/packages/api-core/src/config.test.ts b/packages/api-core/src/config.test.ts index ce68be7..7b7895f 100644 --- a/packages/api-core/src/config.test.ts +++ b/packages/api-core/src/config.test.ts @@ -56,7 +56,7 @@ test('loadConfig prefers persisted config and stores defaults under the user con assert.equal(config.openaiApiKeySource, 'config'); assert.equal(config.dbPath, path.join(home, '.config', 'ghcrawl', 'ghcrawl.db')); assert.equal(config.summaryModel, 'gpt-5-mini'); - assert.equal(config.embeddingBasis, 'title_summary'); + assert.equal(config.embeddingBasis, 'title_original'); assert.equal(config.vectorBackend, 'vectorlite'); }); diff --git a/packages/api-core/src/config.ts b/packages/api-core/src/config.ts index e85bdea..f57726d 100644 --- a/packages/api-core/src/config.ts +++ b/packages/api-core/src/config.ts @@ -357,7 +357,7 @@ export function loadConfig(options: LoadConfigOptions = {}): GitcrawlConfig { { source: 'env', value: getEmbeddingBasis(getEnvString(env, 'GHCRAWL_EMBEDDING_BASIS', 'GHCRAWL_EMBEDDING_BASIS')) }, { source: 'config', value: stored.data.embeddingBasis }, { source: 'dotenv', value: getEmbeddingBasis(getDotenvString(dotenvValues, 'GHCRAWL_EMBEDDING_BASIS', 'GHCRAWL_EMBEDDING_BASIS')) }, - { source: 'default', value: 'title_summary' }, + { source: 'default', value: 'title_original' }, ); const vectorBackend = pickDefined( { source: 'env', value: getVectorBackend(getEnvString(env, 'GHCRAWL_VECTOR_BACKEND', 'GHCRAWL_VECTOR_BACKEND')) }, @@ -403,7 +403,7 @@ export function loadConfig(options: LoadConfigOptions = {}): GitcrawlConfig { opItemName: stored.data.opItemName, summaryModel: summaryModel.value ?? 'gpt-5-mini', embedModel: embedModel.value ?? 'text-embedding-3-large', - embeddingBasis: embeddingBasis.value ?? 'title_summary', + embeddingBasis: embeddingBasis.value ?? 'title_original', vectorBackend: vectorBackend.value ?? 'vectorlite', embedBatchSize, embedConcurrency, diff --git a/skills/ghcrawl/SKILL.md b/skills/ghcrawl/SKILL.md index 375c598..38c44d6 100644 --- a/skills/ghcrawl/SKILL.md +++ b/skills/ghcrawl/SKILL.md @@ -25,8 +25,9 @@ Current pipeline defaults to keep in mind: - persistent semantic search and clustering use a `vectorlite` sidecar index - the default summary model is `gpt-5-mini` -- the default embedding basis is `title_summary`, which means `refresh` may run summarize before embed +- the default embedding basis is `title_original`, so `refresh` does not summarize unless the user explicitly switches to `title_summary` - changing summary model or embedding basis with `ghcrawl configure` makes the next refresh rebuild vectors and clusters +- opting into `title_summary` can materially improve clustering quality, but it adds OpenAI cost; on `openclaw/openclaw` it improved non-solo cluster membership by about 50% Also never run `close-thread` or `close-cluster` unless the user explicitly asks you to mark a local thread or cluster closed. From 013029dbf1dbcbda5fba544fdf9f65c0c588cc9c Mon Sep 17 00:00:00 2001 From: huntharo Date: Fri, 3 Apr 2026 15:01:44 -0400 Subject: [PATCH 31/34] chore: remove .context experiment artifacts --- .../embedding-clustering/spec.yaml | 123 --- .../summary-prompt/experiment-log.yaml | 65 -- .../summary-prompt/prompts/v0-baseline.txt | 1 - .../prompts/v1-strip-boilerplate.txt | 16 - .../prompts/v10-minimal-rules.txt | 3 - .../prompts/v2-signal-focused.txt | 20 - .../summary-prompt/prompts/v3-examples.txt | 19 - .../summary-prompt/prompts/v4-terse.txt | 3 - .../prompts/v5-component-focused.txt | 12 - .../prompts/v6-negative-examples.txt | 16 - .../summary-prompt/prompts/v7-two-pass.txt | 19 - .../prompts/v8-embedding-aware.txt | 19 - .../summary-prompt/prompts/v9-issue-vs-pr.txt | 16 - .../summary-prompt/results/v0-baseline.json | 940 ------------------ .../results/v1-strip-boilerplate.json | 940 ------------------ .../results/v10-minimal-rules.json | 940 ------------------ .../results/v2-signal-focused.json | 940 ------------------ .../summary-prompt/results/v3-examples.json | 940 ------------------ .../summary-prompt/results/v4-terse.json | 940 ------------------ .../results/v5-component-focused.json | 940 ------------------ .../results/v6-negative-examples.json | 940 ------------------ .../summary-prompt/results/v7-two-pass.json | 940 ------------------ .../results/v8-embedding-aware.json | 940 ------------------ .../results/v9-issue-vs-pr.json | 940 ------------------ .../summary-prompt/test-threads.yaml | 92 -- .../vectorlite-clustering/experiment-log.yaml | 225 ----- 26 files changed, 10989 deletions(-) delete mode 100644 .context/compound-engineering/ce-optimize/embedding-clustering/spec.yaml delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/experiment-log.yaml delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v0-baseline.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v1-strip-boilerplate.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v10-minimal-rules.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v2-signal-focused.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v3-examples.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v4-terse.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v5-component-focused.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v6-negative-examples.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v7-two-pass.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v8-embedding-aware.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/prompts/v9-issue-vs-pr.txt delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v0-baseline.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v1-strip-boilerplate.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v10-minimal-rules.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v2-signal-focused.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v3-examples.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v4-terse.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v5-component-focused.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v6-negative-examples.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v7-two-pass.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v8-embedding-aware.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/results/v9-issue-vs-pr.json delete mode 100644 .context/compound-engineering/ce-optimize/summary-prompt/test-threads.yaml delete mode 100644 .context/compound-engineering/ce-optimize/vectorlite-clustering/experiment-log.yaml diff --git a/.context/compound-engineering/ce-optimize/embedding-clustering/spec.yaml b/.context/compound-engineering/ce-optimize/embedding-clustering/spec.yaml deleted file mode 100644 index 722f950..0000000 --- a/.context/compound-engineering/ce-optimize/embedding-clustering/spec.yaml +++ /dev/null @@ -1,123 +0,0 @@ -name: embedding-clustering -description: > - Optimize clustering quality by exploring which embedding source kinds to use - (title, body, dedupe_summary), how to aggregate scores across source kinds - (max, mean, weighted, require-N-sources), and parameter tuning (k, minScore, - maxClusterSize). Uses LLM-as-judge to evaluate cluster coherence and - singleton false-negative rate. - -metric: - primary: - type: judge - name: cluster_coherence - direction: maximize - - degenerate_gates: - - name: solo_pct - check: "<= 95" - description: "Reject if >=95% of clusters are singletons (everything is solo)" - - name: max_cluster_size - check: "<= 500" - description: "Reject mega-clusters that merged unrelated items" - - name: multi_member_pct - check: ">= 5" - description: "At least 5% of threads must be in multi-member clusters" - - diagnostics: - - name: edge_count - - name: cluster_count - - name: solo_clusters - - name: multi_member_clusters - - name: threads_in_multi - - name: total_threads - - name: avg_multi_size - - name: duration_ms - - judge: - model: sonnet - sample_size: 30 - stratification: - - bucket: top_by_size - count: 10 - - bucket: mid_range - count: 10 - - bucket: small_clusters - count: 10 - singleton_sample: 15 - singleton_rubric: | - This thread is currently a SINGLETON — it was not grouped with any other thread. - Given the thread's title and dedupe_summary, evaluate whether this is correct. - - Rate 1-5: - - 5: Clearly unique, no plausible duplicates exist in a repo of 18k issues/PRs - - 4: Probably unique, though a loose connection to other topics is possible - - 3: Uncertain — could go either way, might have related threads - - 2: Likely should be grouped — the topic is common enough to have duplicates - - 1: Obvious false negative — this clearly belongs with other threads - - Return JSON: { score, reasoning } - rubric: | - You are evaluating a cluster of GitHub issues/PRs that were grouped together - by embedding similarity. Each item shows its number, kind (issue/PR), title, - and dedupe_summary. - - Rate this cluster 1-5 for COHERENCE: - - 5: All items clearly about the same specific issue, feature, or component - - 4: Strong theme with minor outliers (1 loosely related item) - - 3: Related topic area but covers 2-3 distinct sub-topics that could be split - - 2: Weak connection — items share superficial similarity only (e.g., same component name but different issues) - - 1: Unrelated items grouped together, no meaningful connection - - Also report: - - distinct_topics: integer — how many distinct sub-topics are in this cluster - - outlier_count: integer — items that don't belong - - dominant_theme: string — 1 sentence describing the main topic - - Return JSON: { score, distinct_topics, outlier_count, dominant_theme, reasoning } - scoring: - primary: mean_score - secondary: - - mean_distinct_topics - - outlier_rate - - singleton_score - sample_seed: 42 - batch_size: 5 - minimum_improvement: 0.3 - max_total_cost_usd: 20 - -measurement: - command: > - node scripts/cluster-optimize-measure.mjs openclaw/openclaw - --backend vectorlite --cluster-mode bounded --max-cluster-size 200 - timeout_seconds: 300 - stability: - mode: stable - -scope: - mutable: - - packages/api-core/src/service.ts - - packages/api-core/src/cluster/ - - packages/api-core/src/openai/provider.ts - - scripts/cluster-optimize-measure.mjs - immutable: - - packages/api-core/src/db/ - -execution: - mode: serial - backend: worktree - max_concurrent: 1 - -parallel: - shared_files: - - "~/.config/ghcrawl/ghcrawl.db" - -constraints: - - "clusterExperiment is read-only on the main DB — it creates a temp vectorlite DB" - - "Do not modify the embedding pipeline or database schema" - - "All experiments must use the same DB (shared_files)" - - "The measurement harness must accept source-kinds and aggregation as CLI args" - -stopping: - max_iterations: 30 - max_hours: 6 - plateau_iterations: 8 diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/experiment-log.yaml b/.context/compound-engineering/ce-optimize/summary-prompt/experiment-log.yaml deleted file mode 100644 index b150e5a..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/experiment-log.yaml +++ /dev/null @@ -1,65 +0,0 @@ -summary_prompt_optimization: - completed: 2026-03-30T18:40:59.650Z - winner: v5-component-focused - runner_up: v2-signal-focused - baseline_score: 2.65 - winner_score: 4.97 - improvement: 87.5% - total_experiments: 11 - test_threads: 40 - results: - - id: v2-signal-focused - avg_score: 4.97 - boilerplate_pct: 0 - cluster_correct_pct: 100 - input_tokens: 47983 - - id: v5-component-focused - avg_score: 4.97 - boilerplate_pct: 0 - cluster_correct_pct: 100 - input_tokens: 45423 - - id: v1-strip-boilerplate - avg_score: 4.95 - boilerplate_pct: 0 - cluster_correct_pct: 100 - input_tokens: 46623 - - id: v3-examples - avg_score: 4.95 - boilerplate_pct: 0 - cluster_correct_pct: 100 - input_tokens: 49863 - - id: v7-two-pass - avg_score: 4.93 - boilerplate_pct: 3 - cluster_correct_pct: 100 - input_tokens: 48503 - - id: v6-negative-examples - avg_score: 4.78 - boilerplate_pct: 3 - cluster_correct_pct: 100 - input_tokens: 47183 - - id: v9-issue-vs-pr - avg_score: 4.75 - boilerplate_pct: 3 - cluster_correct_pct: 100 - input_tokens: 46676 - - id: v8-embedding-aware - avg_score: 4.58 - boilerplate_pct: 5 - cluster_correct_pct: 100 - input_tokens: 47543 - - id: v4-terse - avg_score: 4.47 - boilerplate_pct: 3 - cluster_correct_pct: 100 - input_tokens: 41983 - - id: v10-minimal-rules - avg_score: 4.1 - boilerplate_pct: 25 - cluster_correct_pct: 95 - input_tokens: 40703 - - id: v0-baseline - avg_score: 2.65 - boilerplate_pct: 28 - cluster_correct_pct: 43 - input_tokens: 40423 diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v0-baseline.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v0-baseline.txt deleted file mode 100644 index 8069562..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v0-baseline.txt +++ /dev/null @@ -1 +0,0 @@ -Summarize this GitHub issue or pull request thread. Return concise JSON only with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field should be plain text, no markdown, and usually 1-3 sentences. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v1-strip-boilerplate.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v1-strip-boilerplate.txt deleted file mode 100644 index 4844293..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v1-strip-boilerplate.txt +++ /dev/null @@ -1,16 +0,0 @@ -Summarize this GitHub issue or pull request. Your summary will be embedded as a vector and used for duplicate/similar-issue detection via clustering. - -IGNORE and strip away: -- Issue/PR template sections (### Bug type, ### Steps to reproduce, ### Expected behavior, ### Checklist, etc.) -- Testing instructions, QA notes, deployment steps -- Checkbox lists and checklists -- Version numbers, OS details, and environment info (unless they ARE the bug) -- "Related issues" or "References" sections -- Auto-generated content - -FOCUS on extracting: -- The core problem or feature being described -- Specific technical components, APIs, or subsystems affected -- The root cause or proposed solution (if stated) - -Return concise JSON with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field: plain text, no markdown, 1-3 sentences. The dedupe_summary should capture the essential topic in a way that would match other reports of the same issue. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v10-minimal-rules.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v10-minimal-rules.txt deleted file mode 100644 index f94b9b5..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v10-minimal-rules.txt +++ /dev/null @@ -1,3 +0,0 @@ -Summarize this GitHub issue/PR for duplicate detection. Strip template boilerplate and testing notes. Focus on the specific technical problem or change. - -Return JSON: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Plain text, 1-3 sentences each. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v2-signal-focused.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v2-signal-focused.txt deleted file mode 100644 index 05d8763..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v2-signal-focused.txt +++ /dev/null @@ -1,20 +0,0 @@ -You are extracting the core signal from a GitHub issue or pull request for duplicate detection. The output will be embedded and clustered with similar issues. - -Your job is to answer: "What is this issue/PR fundamentally about?" Ignore everything else. - -Strip away ALL of the following — they add noise to embeddings: -- Template boilerplate (headings like "### Steps to reproduce", "### Expected behavior") -- Testing notes, QA instructions, review checklists -- Environment details (OS, browser, versions) unless they ARE the core issue -- Deployment procedures, rollback plans -- Links, references to other issues -- Formatting artifacts (checkboxes, horizontal rules, badges) - -What to capture: -- The specific bug, feature, or change in concrete terms -- Which component/module/API/subsystem is affected -- Why it matters (the user impact, not the testing impact) - -Return JSON with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. -Rules: plain text only, no markdown, 1-3 sentences each. -dedupe_summary is the most important field — it must be a clean, specific description that would match other reports about the same underlying issue. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v3-examples.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v3-examples.txt deleted file mode 100644 index 5720eeb..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v3-examples.txt +++ /dev/null @@ -1,19 +0,0 @@ -Summarize this GitHub issue or pull request for duplicate detection clustering. The dedupe_summary will be embedded as a vector — it must be clean signal with zero noise. - -REMOVE all template noise. Examples of what to strip: -- "### Bug type\nRegression (worked before, now fails)" → ignore, it's a template field -- "### Steps to reproduce\n1. Go to settings\n2. Click..." → ignore reproduction steps -- "### Expected behavior\nIt should work" → ignore -- "- [x] Tests added\n- [x] Docs updated" → ignore checklist -- "### Environment\nOS: macOS 14.2, Node 20.1" → ignore unless the OS IS the bug -- "Fixes #1234" → ignore cross-references -- "## Testing\nRun `pnpm test`..." → ignore testing instructions - -KEEP the core signal: -- "Discord REST API ignores proxy configuration" → THIS is the signal -- "WhatsApp group messages not delivered when streaming enabled" → THIS is the signal -- "Telegram media download hangs on large video files" → THIS is the signal - -Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. -Plain text, no markdown, 1-3 sentences each. -dedupe_summary must be a specific, searchable description of the core issue or change. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v4-terse.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v4-terse.txt deleted file mode 100644 index 08b7a54..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v4-terse.txt +++ /dev/null @@ -1,3 +0,0 @@ -Extract the core topic from this GitHub issue or PR for duplicate clustering. Ignore all template boilerplate, testing notes, checklists, environment details, and reproduction steps. Focus only on WHAT the issue/change is about technically. - -Return JSON: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Plain text, 1-2 sentences each. Be extremely concise — every word in dedupe_summary must add signal for finding duplicates. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v5-component-focused.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v5-component-focused.txt deleted file mode 100644 index 6ccdab9..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v5-component-focused.txt +++ /dev/null @@ -1,12 +0,0 @@ -Summarize this GitHub issue or pull request for automated duplicate detection. Your summary will be embedded and clustered. - -Structure your analysis: -1. First identify the COMPONENT or SUBSYSTEM (e.g., "Discord gateway", "WhatsApp delivery", "Telegram media handler", "CLI routing", "session management") -2. Then identify the SPECIFIC PROBLEM or CHANGE within that component -3. Combine into a clear dedupe_summary that starts with the component name - -Ignore completely: template boilerplate, testing instructions, checklists, environment info, reproduction steps, deployment notes, version numbers, cross-references. - -Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. -Plain text, no markdown, 1-3 sentences each. -dedupe_summary format: "[Component]: [specific issue or change]" — this helps cluster by subsystem. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v6-negative-examples.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v6-negative-examples.txt deleted file mode 100644 index c98f3c7..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v6-negative-examples.txt +++ /dev/null @@ -1,16 +0,0 @@ -Summarize this GitHub issue or pull request. The dedupe_summary field will be used for vector-based duplicate detection. - -BAD dedupe_summary examples (too generic, would match unrelated issues): -- "Bug fix for an issue with the application" ← too vague -- "Updates configuration handling and fixes a reported problem" ← no specifics -- "Resolves issue where feature doesn't work as expected" ← says nothing - -GOOD dedupe_summary examples (specific, would cluster correctly): -- "Discord REST API requests bypass proxy configuration, causing fetch failures in proxy-required environments" -- "WhatsApp group-level systemPrompt configuration not applied to message delivery" -- "Telegram editMessageCaption fails for non-photo media types due to missing content-type detection" - -Remove from your analysis: template headers, checklists, testing instructions, environment details, reproduction steps, deployment notes. These add noise to embeddings. - -Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. -Plain text, no markdown, 1-3 sentences each. dedupe_summary is the most critical field. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v7-two-pass.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v7-two-pass.txt deleted file mode 100644 index f02325d..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v7-two-pass.txt +++ /dev/null @@ -1,19 +0,0 @@ -You will summarize a GitHub issue or pull request in two mental passes: - -PASS 1 — STRIP: Mentally remove all of the following before summarizing: -- Template sections: headers like "### Bug type", "### Steps to reproduce", "### Checklist", "## Testing" -- Checkboxes: "- [x] Tests added", "- [ ] Docs updated" -- Environment details: OS versions, browser versions, Node versions -- Reproduction steps: numbered step lists -- Testing instructions: "Run `npm test`", "Deploy to staging" -- Cross-references: "Fixes #1234", "Related to #5678" -- Formatting: horizontal rules, badges, images unless they show the bug - -PASS 2 — EXTRACT: From what remains, identify: -- What specific component or API is affected? -- What exactly goes wrong or what change is being made? -- What is the user-facing impact? - -Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. -All fields: plain text, no markdown, 1-3 sentences. -dedupe_summary: the essential topic that would match other reports of the same issue. Be specific enough to distinguish from similar-but-different issues. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v8-embedding-aware.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v8-embedding-aware.txt deleted file mode 100644 index 9051092..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v8-embedding-aware.txt +++ /dev/null @@ -1,19 +0,0 @@ -This text is a GitHub issue or pull request. You must produce a summary optimized for text embedding and cosine-similarity-based duplicate detection. - -Key insight: the dedupe_summary will be converted to a 3072-dimensional vector. Similar vectors cluster together. Your goal is to maximize the chance that reports about the SAME underlying issue produce SIMILAR vectors, while reports about DIFFERENT issues produce DISSIMILAR vectors. - -What helps similarity detection: -- Specific technical terms (API names, component names, error messages) -- The core behavior being reported or changed -- Affected subsystem or module names - -What hurts similarity detection (creates false matches or misses): -- Generic words: "bug", "fix", "issue", "problem", "update", "change" -- Template boilerplate that appears in many unrelated issues -- Testing details, reproduction steps, environment info -- Version numbers, dates, PR metadata - -Strip all boilerplate. Extract only the technical signal. - -Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. -Plain text, no markdown, 1-3 sentences each. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v9-issue-vs-pr.txt b/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v9-issue-vs-pr.txt deleted file mode 100644 index c982729..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/prompts/v9-issue-vs-pr.txt +++ /dev/null @@ -1,16 +0,0 @@ -Summarize this GitHub issue or pull request for duplicate detection clustering. - -If this is an ISSUE: -- Extract: What specific problem is reported? Which component/API/feature is broken? -- Ignore: reproduction steps, expected vs actual behavior headings, environment details, template fields -- dedupe_summary should describe THE PROBLEM in specific technical terms - -If this is a PULL REQUEST: -- Extract: What specific problem does this fix? What change is being made and to what component? -- Ignore: testing instructions, review checklists, deployment notes, CI status, diff statistics -- dedupe_summary should describe THE FIX/CHANGE and what problem it addresses - -In both cases, strip template boilerplate, checklists, and any content that would appear identically in unrelated issues/PRs. - -Return JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. -Plain text, no markdown, 1-3 sentences each. dedupe_summary is most important — it must uniquely identify this topic. \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v0-baseline.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v0-baseline.json deleted file mode 100644 index 0a99446..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v0-baseline.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v0-baseline", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "Summarize this GitHub issue or pull request thread. Return concise JSON only with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field should be plain text, no markdown, and usually 1-3 sentences.", - "timestamp": "2026-03-30T15:44:34.227Z", - "aggregate": { - "avg_score": 2.65, - "avg_signal_density": 2.65, - "boilerplate_count": 11, - "boilerplate_pct": 28, - "would_cluster_correctly_pct": 43, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 40423, - "total_output_tokens": 24956 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "channels.discord.proxy only proxies the Discord WebSocket gateway; startup REST API calls (deploy commands, fetch bot identity) and the guild resolution path do not use the configured HTTP proxy, causing REST requests to time out and guild channels to be non-functional in environments that require a proxy.", - "solution_summary": "Apply the configured proxy to the undici HTTP dispatcher for all Discord REST calls (not just the gateway), ensure the guild-resolve code path uses the same proxied HTTP client and add retry logic; optionally add a global proxy config (e.g., gateway.proxy or network.proxy) to set a global ProxyAgent.", - "maintainer_signal_summary": "Issue labeled bug and regression; reporter provided logs, reproduction steps, and a partial workaround (global ProxyAgent) that fixes REST but not guild resolve; no maintainer comments or fixes in the thread yet.", - "dedupe_summary": "No duplicate referenced in the thread; this appears to be a regression distinct from WebSocket proxying because guild-resolve uses a different HTTP path — check other proxy-related issues but treat this as a new, actionable bug." - }, - "judge": { - "score": 3, - "has_boilerplate": true, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Identifies the core regression (REST/guild-resolve not proxied vs WebSocket) but includes meta/instructional noise ('No duplicate referenced', 'check other proxy-related issues') that reduces clarity and conciseness for deduplication." - }, - "usage": { - "input_tokens": 2146, - "output_tokens": 556 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "Outbound Discord REST requests are not using the configured channels.discord.proxy, so sending messages/attachments fails with fetch failed in restricted networks while the WebSocket gateway (receiving) works via the proxy.", - "solution_summary": "Add proxy support to the Discord RequestClient/fetch implementation (or pass a proxy agent to undici/node fetch) so all REST calls and media uploads/downloads honor channels.discord.proxy; add tests and docs for proxy behavior.", - "maintainer_signal_summary": "No maintainer responses in the thread; only a user bug report (labelled bug) with environment/version details and reproduction steps.", - "dedupe_summary": "Likely duplicates other requests for Discord REST/RequestClient proxy support or undici fetch proxying; search existing issues for \"discord proxy\", \"RequestClient proxy\", or \"undici proxy\" before filing a new one." - }, - "judge": { - "score": 3, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Mentions the relevant components (Discord REST/RequestClient and undici proxy support) but reads like a duplicate suggestion rather than a concise statement that outbound REST requests aren't using the configured proxy, so it has useful keywords but misses the core failure detail." - }, - "usage": { - "input_tokens": 828, - "output_tokens": 584 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "When OpenClaw is configured to use an HTTP proxy for Discord, the provider can connect to the gateway and log in and can fetch the application ID, but some startup REST calls (deploying native commands and fetching bot identity) consistently fail with \"fetch failed\", leaving the Discord provider partially functional and triggering periodic stale-socket restarts.", - "solution_summary": "No confirmed fix in thread; suspected root cause is inconsistent proxy handling where some Discord REST requests bypass the proxy or use a different fetch/dispatcher path. Recommended action is to ensure all REST calls use the same proxy-aware fetch path; the reporter offered to provide more details or test a patch.", - "maintainer_signal_summary": "No maintainer responses in the thread; the reporter supplied logs, environment details, reproduction notes, and offered to assist with testing.", - "dedupe_summary": "Not marked as a duplicate; distinct because the gateway/login succeeds while specific REST startup calls fail, indicating inconsistent proxy usage rather than token/config errors; should be compared to other proxy-related reports but appears unique. " - }, - "judge": { - "score": 3, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "The summary correctly highlights the core issue (gateway login succeeds while REST startup calls fail due to inconsistent proxy usage) but includes meta phrasing and omits the specific failing REST operations (native command deployment, bot identity fetch), so it's adequate for proxy-related clustering but could be more precise." - }, - "usage": { - "input_tokens": 974, - "output_tokens": 549 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "OpenClaw reports Discord proxy as enabled but REST API calls fail with \"fetch failed\" under WSL2 using an HTTP proxy; WebSocket/gateway never establishes though curl via the same proxy succeeds. The Discord channel is thus completely non-functional despite correct proxy config in ~/.openclaw/openclaw.json.", - "solution_summary": "No fix provided in the thread. Reporter notes the proxy appears logged but not applied to the underlying Discord.js/fetch client; suggested workarounds are using alternative channels (Feishu) or a TUN-mode transparent proxy.", - "maintainer_signal_summary": "No maintainer response in the thread. The reporter supplied reproducible steps, logs, config, environment (OpenClaw 2026.3.13 on WSL2), and labeled severity as high.", - "dedupe_summary": "No duplicates referenced; issue appears unique and 100% reproducible when an HTTP proxy is required, likely needing a code change to ensure the proxy agent is passed to Discord.js/fetch clients." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary that captures the core problem — Discord proxy is recognized but REST calls fail because the proxy agent isn't passed to Discord.js/fetch — and suggests the needed code change, with minimal noise." - }, - "usage": { - "input_tokens": 811, - "output_tokens": 696 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "Discord REST API calls from OpenClaw ignore the configured proxy and fail with \"TypeError: fetch failed\" when sending messages, while the WebSocket connection and curl via the same proxy work; Telegram and Discord WebSocket honor the proxy so the issue is specific to Discord REST requests.", - "solution_summary": "Make Discord REST requests use the same proxy-aware SSRF-guarded fetch path implemented for web_search/web_fetch so they honor channels.discord.proxy and HTTP(S)_PROXY environment variables.", - "maintainer_signal_summary": "No maintainer response in the thread; reporter points to the 2026.2.26 changelog that fixed web_tools proxying but did not include Discord REST and requests that Discord REST be included in the fix; a webhook workaround was suggested.", - "dedupe_summary": "Related to the 2026.2.26 web_tools proxy fix but not a duplicate; should be tracked alongside web_fetch/web_search proxy issues and any other channel REST proxy reports." - }, - "judge": { - "score": 2, - "has_boilerplate": false, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "The summary is meta (references a specific release and related fixes) rather than stating the core bug (Discord REST API ignoring proxy and fetch failures), so it lacks the concrete problem description needed for reliable deduplication." - }, - "usage": { - "input_tokens": 834, - "output_tokens": 745 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "In v2026.3.11 the CLI command 'openclaw agent --channel discord --to ...' routes deliveries to agent:*:main instead of the live Discord channel/thread session because the session resolver does not search by delivery context, the command path omits channel/account context, Discord targets are not normalized, and the compact resolver lookup is incorrect.", - "solution_summary": "A manual patch was applied to compiled dist files to add delivery-context-based session resolution, normalize Discord targets, pass channel and replyAccount/accountId through the command path, and fix the compact resolver; post-patch validation shows messages route to the correct channel/thread session.", - "maintainer_signal_summary": "This is high-risk because the fix was applied to compiled dist files and will be lost on updates; maintainers should implement the fix in source, add tests, and include a post-update revalidation step. Affected compiled files and related issue numbers are listed in the report.", - "dedupe_summary": "Likely a regression related to existing reports about session routing fallbacks and Discord thread leakage (issues #36401, #41562, #16149); consider de-duplicating and tracking under a canonical regression issue. " - }, - "judge": { - "score": 2, - "has_boilerplate": true, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "The summary is too generic and meta—it only flags this as a regression and links other issues instead of describing the specific CLI session-resolver bug (delivery-context lookup, Discord target normalization), so it won't reliably cluster similar technical reports." - }, - "usage": { - "input_tokens": 761, - "output_tokens": 765 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions affecting Discord live sessions: volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) was re-injected every turn causing context bloat and degradation, and CLI commands routed to the wrong session (defaulting to agent:*:main instead of the thread-specific session).", - "solution_summary": "Patched compiled runtime bundles to stop reinjecting thread metadata, added session resolution by actual delivery target and delivery-context lookup, propagated channel and replyAccount/accountId to the resolver, and corrected use of buildAgentMainSessionKey. Service was restarted and sessions verified to show correct toolCall outputs and no repeated context.", - "maintainer_signal_summary": "Changes were applied as manual edits to compiled bundles in openclaw/dist and verified by restarting openclaw-gateway.service; maintainers note this patch will be lost on update and recommend keeping a re-application script. Environment: OpenClaw 2026.3.11, Discord channel, systemd deployment.", - "dedupe_summary": "Treat as a single issue with two related root causes (runtime metadata injection and CLI resolver context); track by affected files (pi-embedded, gateway-cli, compact, register.agent) and symptoms (context bloat in threads, CLI commands landing in main). Mark as high priority and avoid reopening duplicates that describe the same Discord-thread metadata reinjection or CLI routing behavior." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise summary that captures both core regressions (volatile metadata re-injection in Discord threads and CLI session resolver routing), affected components, and symptoms, with only a minor non-deduplication directive that isn't necessary for clustering." - }, - "usage": { - "input_tokens": 680, - "output_tokens": 620 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "The sandbox media staging limit is hardcoded to 5 MB in dist/store-*.js (MEDIA_MAX_BYTES = 5 * 1024 * 1024), causing files larger than 5 MB to be rejected or silently fail in sandboxed agents. This regression exists in v2026.3.7 and v2026.3.8 and requires a manual post-install patch as a workaround.", - "solution_summary": "Expose the MEDIA_MAX_BYTES as a configurable option (e.g., tools.media.maxBytes in openclaw.json) or read the limit from existing per-type config, remove the hardcoded constant from built files, and apply a sensible default when unset so larger files can be allowed per user configuration.", - "maintainer_signal_summary": "Issue is labeled bug and regression and includes version, OS, repro steps and a workaround, but there are no maintainer comments or fixes in the thread yet.", - "dedupe_summary": "Likely unique but could overlap with other issues about media size limits or config not honored; check for prior issues mentioning MEDIA_MAX_BYTES or sandbox media size to avoid duplication." - }, - "judge": { - "score": 1, - "has_boilerplate": false, - "signal_density": 1, - "would_cluster_correctly": false, - "reasoning": "The summary fails to state the core bug (sandbox MEDIA_MAX_BYTES hardcoded to 5MB and not user-configurable) and instead gives meta guidance about uniqueness, so it’s noisy and won’t help cluster similar issues." - }, - "usage": { - "input_tokens": 727, - "output_tokens": 547 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "Discord bot can receive messages via the Gateway but cannot send them: all REST API calls fail with 'fetch failed' despite proxy configured in openclaw.json, gateway logs showing 'rest proxy enabled', and curl requests using the same proxy succeeding. Environment: OpenClaw 2026.2.25 on macOS with Node.js 25.6.1.", - "solution_summary": "No resolution recorded in the thread. Suggested next actions are to verify OpenClaw actually applies the configured proxy to its REST client (and whether the underlying HTTP client supports proxies), try setting HTTP_PROXY/HTTPS_PROXY environment variables, collect full stack traces and debug logs, and reproduce a failing REST call from Node to isolate whether undici/fetch is bypassing the proxy.", - "maintainer_signal_summary": "Gateway logs indicate 'rest proxy enabled' and 'gateway proxy enabled' but also show repeated 'fetch failed' errors and failed native command deployment/fetching bot identity; no maintainer responses or fixes are included in the thread.", - "dedupe_summary": "No duplicates or related issues referenced in the thread; appears to be a new bug report specific to REST requests and proxy handling." - }, - "judge": { - "score": 2, - "has_boilerplate": true, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "The summary is mostly metadata ('No duplicates...') and too generic—it fails to mention Discord, the 'fetch failed' error, or that REST requests via configured proxy are failing—so it won't reliably cluster similar issues." - }, - "usage": { - "input_tokens": 672, - "output_tokens": 810 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "@buape/carbon's RequestClient (and other globalThis.fetch call sites like memorySearch) do not honor channels.discord.proxy from openclaw.json, so REST API calls and some media downloads bypass the configured proxy while the WebSocket gateway correctly uses it. The root cause is RequestClient calling globalThis.fetch without a proxy-aware undici dispatcher and Node 22's native fetch not using env proxy vars by default.", - "solution_summary": "Change @buape/carbon's REST client to accept an optional dispatcher/proxy configuration and have OpenClaw pass channels.discord.proxy into it so REST calls use the same proxy as the gateway. Short-term workaround: preload undici's EnvHttpProxyAgent via NODE_OPTIONS to set a global dispatcher.", - "maintainer_signal_summary": "No maintainer responses or actions recorded in the thread; the report includes a clear bug description, root cause analysis, a workaround, and an explicit suggested fix.", - "dedupe_summary": "Related to issue #25232 (attachment downloads bypass proxy) which shares the same root cause but affects a different code path; consider linking or consolidating fixes rather than duplicating efforts." - }, - "judge": { - "score": 2, - "has_boilerplate": false, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "The summary only references a related issue and suggests consolidation without describing the actual technical problem (Discord REST API bypassing channels.discord.proxy), so it lacks the specific signal needed for reliable deduplication." - }, - "usage": { - "input_tokens": 681, - "output_tokens": 660 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "Under sustained load and long-running conversations OpenClaw experiences unbounded context token growth, increasing per-turn latency, Discord lane blocking/serialization under load, and typing indicators stuck until TTL in some paths.", - "solution_summary": "Applied configuration limits and timeouts: reduced concurrent session/agent/subagent limits, set agents.defaults.contextTokens to 80000, increased Discord eventQueue.listenerTimeout to 300000, improved patching script to cover hash variants, and restarted the gateway; post-restart no new lane-wait or typing-TTL errors observed.", - "maintainer_signal_summary": "Maintainers implemented and validated the config changes and hardening script, restarted the gateway, and confirmed the immediate errors stopped while noting restart is a mitigation not a full fix; they recommended making limits adaptive or exposing them as tunable config options.", - "dedupe_summary": "Related issues #27690 (lane blocking), #44026 (serialization by agentId), and #26733/#27172 (typing TTL) overlap with this root-cause; treat this thread as the umbrella/root fix and link or mark the related issues as duplicates or dependent. " - }, - "judge": { - "score": 2, - "has_boilerplate": false, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "The summary is an action note about linking related issue IDs rather than a concise description of the core problem (unbounded context growth, lane blocking, typing TTL under load), so it provides low useful signal for deduplication and won't reliably cluster similar technical reports." - }, - "usage": { - "input_tokens": 615, - "output_tokens": 719 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, causing context contamination, token bloat, prompt cache busting and degraded performance. The compiled runtime lacks a guard to limit this injection to the first turn, unlike prior Slack/iMessage fixes.", - "solution_summary": "A manual patch to compiled dist files prevents repeated injection and adds an isNewSession guard: dist/pi-embedded-D6PpOsxP.js stops injecting the listed metadata, and dist/compact-1mmJ_KWL.js adds the isNewSession check and replaces resolveAgentMainSessionKey with buildAgentMainSessionKey. Post-patch validation shows threads no longer repeat context and prompt caching is restored.", - "maintainer_signal_summary": "High regression risk because fixes were applied directly to compiled dist/ files and will be overwritten by updates; maintainers should apply the fix in source, include tests/guards, and add a post-update revalidation script. Issue is urgent and related to several open threads.", - "dedupe_summary": "Related issues appear to be duplicates or closely related (#41355, #34228, #37047) and should be consolidated under a single upstream fix for Discord thread metadata injection to avoid repeated reports." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Clearly identifies the core problem (Discord thread metadata injection) and intent to consolidate duplicates, but omits specific technical symptoms (which fields, cache/token impact) and includes issue-number noise." - }, - "usage": { - "input_tokens": 627, - "output_tokens": 530 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord thread messages repeatedly re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, causing context pollution, prompt cache misses, increased token usage and degraded performance in long threads. The compiled runtime in dist/pi-embedded-D6PpOsxP.js treats Discord like other platforms but lacks guards limiting injection to the first turn.", - "solution_summary": "A local manual patch to dist/pi-embedded-D6PpOsxP.js stops injecting the volatile Discord metadata after the first turn; post-patch validation shows eliminated repeated context, restored prompt caching, and reduced token usage. The change was applied directly to compiled files and verified, but not yet merged into source.", - "maintainer_signal_summary": "Reporter applied a high-risk manual fix and requests an official source-level patch plus a post-update revalidation script because edits to dist/ will be overwritten by updates. No maintainer merge or official release yet; action recommended is to implement the fix in source and release a patched build.", - "dedupe_summary": "Related/overlapping issues: #41355 (ThreadStarterBody re-injected), #34228 (thread sessions reset), #37047 (starter metadata busting cache). This appears to be the same root cause as prior Slack/iMessage fixes and should be linked or deduplicated with those tickets." - }, - "judge": { - "score": 3, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": false, - "reasoning": "The summary correctly links related issues and suggests a shared root cause but fails to state the specific bug (Discord thread metadata re-injection) or key affected file, so it has useful signal but is missing core details for reliable deduplication." - }, - "usage": { - "input_tokens": 635, - "output_tokens": 632 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "Two runtime regressions affecting Discord threads: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing unbounded context growth and performance degradation, and the session resolver falling back to a global agent:*:main so CLI/session routing resolves to the wrong session.", - "solution_summary": "Manual patches were applied to compiled runtime bundles (several dist/*.js files) to fix context propagation and resolver lookup by delivery context, with an idempotent re-application script and timestamped backups created; gateway was restarted and verified, and a verification checklist completed.", - "maintainer_signal_summary": "The fixes are manual edits to compiled bundles and will be lost on updates; maintainers should upstream the changes into source/builds, keep the re-apply script and backups, and add smoke/long-thread tests and monitoring to prevent regressions.", - "dedupe_summary": "Related to existing issues about context growth in long conversations, CLI session resolution, and Discord thread handling; treat this as the runtime/patch instance of those regressions and consolidate tracking under those prior reports. " - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "The summary clearly identifies the core issues — context growth in long conversations, CLI/session resolution, and Discord thread handling as a runtime/patch instance to consolidate with prior reports — but omits some specific technical artifacts and field names present in the original which would be helpful for perfect matching." - }, - "usage": { - "input_tokens": 611, - "output_tokens": 525 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "OpenClaw receives Discord messages and the model generates replies, but sending the final reply fails with \"TypeError: fetch failed\" despite logs showing both gateway and REST proxy enabled and the proxy working for curl. Occurs in DMs and group chats on OpenClaw 2026.3.2 running under Node 22.22.0.", - "solution_summary": "No fix or patch provided in the thread. Likely causes are the HTTP fetch call for sending replies not honoring the configured proxy or a runtime/fetch compatibility issue; recommended actions are to add fetch-stack debug logs, confirm proxy usage in the REST send path, reproduce with an equivalent curl request, and provide stack traces.", - "maintainer_signal_summary": "No maintainer comments or triage activity in the thread; the issue was opened and labeled bug and regression but not acted on yet.", - "dedupe_summary": "No duplicate issues referenced; appears to be a unique report." - }, - "judge": { - "score": 1, - "has_boilerplate": false, - "signal_density": 1, - "would_cluster_correctly": false, - "reasoning": "The summary does not describe the core technical problem (Discord proxy fetch failures when sending replies) and only states uniqueness, so it provides no useful signal for deduplication." - }, - "usage": { - "input_tokens": 551, - "output_tokens": 577 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "In v2026.3.11 the CLI command openclaw agent --channel discord --to resolves to agent:*:main instead of the live Discord channel/thread session because the compiled session resolver does not look up sessions by delivery context (channel+to+accountId) and Discord targets are not normalized (e.g. \"148...\" vs \"channel:148...\").", - "solution_summary": "A manual patch was applied to dist/gateway-cli-BjsM6fWb.js to normalize Discord targets, resolve sessions by the real delivery context (channel+to+accountId), and lookup existing sessions before falling back to agent:*:main; post-patch validation shows messages route to the correct channel/thread session.", - "maintainer_signal_summary": "This is a high-priority regression that requires an upstream source-level fix and tests to persist across releases; the current hotfix is high-risk because it edits compiled files in dist and will be lost on reinstall/upgrade.", - "dedupe_summary": "Related to #41562 (Discord thread session delivery leaking to main session) and references #36401 (feature request for --session-key); likely a duplicate/should be tracked together with #41562." - }, - "judge": { - "score": 2, - "has_boilerplate": false, - "signal_density": 2, - "would_cluster_correctly": true, - "reasoning": "The summary just marks the issue as related/duplicate and briefly mentions a Discord session leak without the key technical details (CLI flag, resolver behavior, file involved), so it's too generic and low-signal for reliable deduplication though it may still link to the referenced duplicate." - }, - "usage": { - "input_tokens": 616, - "output_tokens": 856 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "In v2026.3.11 the compiled file dist/compact-1mmJ_KWL.js calls a non-existent function resolveAgentMainSessionKey, causing ReferenceError at runtime; the agent command fails and gateways can crash. This regression was introduced by the prior session-resolver patch.", - "solution_summary": "Manually edited dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...), which removes the ReferenceError and restores agent command and session behavior.", - "maintainer_signal_summary": "High priority and high risk: this is a regression in compiled/dist files that will be lost on update. Maintainters should fix the source, rebuild and publish a new release that includes this change, and ensure the fix is applied along with the earlier session-resolver fix.", - "dedupe_summary": "This is a follow-up regression to fix #3 (session resolver); fix #5 depends on fix #3 and both must be included in releases to avoid the ReferenceError." - }, - "judge": { - "score": 2, - "has_boilerplate": false, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "The summary is vague and refers to internal issue numbers/dependencies rather than the concrete regression (incorrect use of resolveAgentMainSessionKey vs buildAgentMainSessionKey in the compiled compact bundle), so it offers poor, noisy signal for deduplication." - }, - "usage": { - "input_tokens": 587, - "output_tokens": 608 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "Discord plugin's channels.discord.proxy setting is applied only to WebSocket connections, not REST API calls, causing health checks and message sends to fail with \"fetch failed\" in proxy-required environments despite curl working via the same proxy.", - "solution_summary": "Ensure the channels.discord.proxy value is used by the Discord REST HTTP client and healthcheck (route all HTTP requests through the configured proxy), add tests for proxied REST calls, and release a patch updating the plugin to use the proxy for both REST and WebSocket traffic.", - "maintainer_signal_summary": "Issue labeled bug and regression; no maintainer comments, assignment, or PR linked yet.", - "dedupe_summary": "Not marked as a duplicate and no related issues referenced; appears to be a new regression bug." - }, - "judge": { - "score": 1, - "has_boilerplate": false, - "signal_density": 1, - "would_cluster_correctly": false, - "reasoning": "This summary contains only procedural/meta info and omits the core technical issue (Discord proxy setting not applied to REST API calls), so it would not help cluster similar bugs." - }, - "usage": { - "input_tokens": 543, - "output_tokens": 552 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the compiled session resolver in dist/compact-1mmJ_KWL.js does not lookup sessions by delivery context (channel+to+accountId), so agent commands fall back to agent:*:main instead of resolving the live channel/session, causing routing and response association failures.", - "solution_summary": "A local manual patch to dist/compact-1mmJ_KWL.js adds delivery-context lookup (channel+to+accountId) in the agent session resolver and retains fallback to main; post-patch validation shows correct session resolution and consistent routing.", - "maintainer_signal_summary": "High regression risk: this is a compiled dist/ file change that will be overwritten by updates; maintainers should implement the fix in source/build, release an official patch, and prioritize preventing recurrence in future builds.", - "dedupe_summary": "Unique to v2026.3.11 and file dist/compact-1mmJ_KWL.js: session resolver missing delivery-context lookup; check for other reports in this release but treat as a single-source build artifact issue requiring source-level fix." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise and specific about the core problem (session resolver in dist/compact-1mmJ_KWL.js missing delivery-context lookup), but includes minor triage/action noise (release-uniqueness and treat-as-build-artifact guidance) and an unnecessary version mention." - }, - "usage": { - "input_tokens": 557, - "output_tokens": 486 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "The Control UI has most interface text hardcoded in English, creating usability barriers for non-English speakers (notably Chinese) across settings, buttons, agent/skill pages, channel configuration, and status messages.", - "solution_summary": "Extract all hardcoded strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, provide a language selector or auto-detect system language, and create infrastructure to add more languages later.", - "maintainer_signal_summary": "No maintainer feedback shown in the thread; the reporter asks if this is on the roadmap and invites contributors, marking the request Medium-High priority for UX/accessibility.", - "dedupe_summary": "Project docs already include zh-CN translations and the control-ui bundle contains hardcoded strings; recommend checking for existing i18n issues or PRs to avoid duplicate work before proceeding." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary captures the key facts (docs have zh-CN, control-ui has hardcoded strings) but adds a minor coordination recommendation instead of succinctly stating the core request to add i18n support and locale files." - }, - "usage": { - "input_tokens": 478, - "output_tokens": 544 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "WhatsApp channel could not apply channel-configured systemPrompt instructions at the account or group level, preventing per-account or per-group prompt customization and wildcard defaults.", - "solution_summary": "Adds systemPrompt support to WhatsApp config/schema with a resolution hierarchy (root account prompt overridden by account prompt; group prompt uses specific group then wildcard; account groups fully replace root groups). Implementation includes prompt composition (account prompt + resolved group prompt), tests, and docs updates.", - "maintainer_signal_summary": "Backwards compatible, WhatsApp-only change with no hot-reload/allowlist/mention-gating semantics altered; includes tests and docs and addresses issue #7011; PR is focused and will replace a previous broader PR.", - "dedupe_summary": "This PR supersedes the author's prior PR #40250 by providing a narrower change set; account-level groups replace root groups (no merge) and wildcard '*' semantics remain supported to avoid duplicate configs." - }, - "judge": { - "score": 2, - "has_boilerplate": true, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "Summary emphasizes PR precedence and merge behavior (including a PR number) instead of the core feature—adding WhatsApp account/group systemPrompt support—so it contains noise and would not reliably cluster with similar feature changes." - }, - "usage": { - "input_tokens": 2254, - "output_tokens": 390 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before agent processing, and group policy resolution could ignore account-scoped config so account-scoped groupAllowFrom/allowFrom fallbacks could fail, causing valid group messages to be rejected.", - "solution_summary": "Enable append processing only for WhatsApp group messages and propagate accountId when resolving WhatsApp group policy so account-scoped groupAllowFrom/allowFrom are considered; keep DM append history skipped and preserve existing inbound dedupe behavior.", - "maintainer_signal_summary": "Bugfix touching src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts; no schema or dist changes, targeted tests added for account-scoped allowlist fallback, validated against live repro; backward compatible but watch for backlog/history processing risks and account-scoped behavior changes.", - "dedupe_summary": "Inbound dedupe remains in place and DM append history continues to be skipped to avoid replay; append processing is scoped to groups to prevent duplicate or unexpected processing of historical DMs." - }, - "judge": { - "score": 3, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": false, - "reasoning": "Summmary correctly highlights preserving inbound dedupe and scoping append processing to groups, but omits the important account-scoped policy/gating change, so it may miss duplicates related to WhatsApp account-scoped policy resolution." - }, - "usage": { - "input_tokens": 1806, - "output_tokens": 572 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because resolveClientIp() returned undefined for requests from a trusted proxy that omitted forwarding headers, and isLocalDirectRequest() failed to fall back to the socket address when allowRealIpFallback was enabled and XFF resolved to a non-loopback Tailscale IP.", - "solution_summary": "resolveClientIp() now falls back to the socket remoteAddress when no forwarding headers are present; isLocalDirectRequest() checks req.socket.remoteAddress when allowRealIpFallback is enabled; isLocalishHost() gates .ts.net acceptance behind the existing allowTailscale flag and that flag is threaded through callers; docs updated and tests added.", - "maintainer_signal_summary": "All tests updated and passing; change is backward compatible and preserves existing behavior when tailscale.mode=\"serve\" because allowTailscale defaults to true; users behind Tailscale Serve should set gateway.trustedProxies and gateway.allowRealIpFallback as documented; no new auth modes introduced and this is a security hardening fix.", - "dedupe_summary": "This closes issue #28472 and depends on PR #50636 for overlapping allowTailscale gating; no other duplicate issues noted." - }, - "judge": { - "score": 1, - "has_boilerplate": true, - "signal_density": 1, - "would_cluster_correctly": false, - "reasoning": "The summary only references issue/PR numbers and dependency info and contains no technical description of the actual bugfix or changes, so it won't help cluster similar issues." - }, - "usage": { - "input_tokens": 1645, - "output_tokens": 609 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "checkInboundAccessControl computed an isSelfChat flag but never used it as a guard, so when WhatsApp channel config lacked an explicit groupPolicy non-self messages (groups, stranger DMs, broadcasts) could pass through even in self-chat deployments, violating the intended isolation boundary.", - "solution_summary": "Add an early-return guard in checkInboundAccessControl that rejects any non-self message when account.selfChatMode === true, placed before group/DM policy evaluation; the change checks the explicit config flag (not the heuristic isSelfChat) and preserves isSelfChat in results for UI/logging.", - "maintainer_signal_summary": "All 9 existing access-control tests pass; author verified code paths and scenarios, limited the change to src/web/inbound/access-control.ts, documented intentional behavior (auto-detected self-chat unchanged), and provided quick-revert instructions; some review conversations remain for maintainer judgment.", - "dedupe_summary": "This PR is the canonical fix for explicit selfChatMode enforcement; it does not affect auto-detected self-chat heuristics or other channels/policies and may be followed by a separate change to handle heuristic-only cases if desired." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise and focused on enforcing explicit selfChatMode and excluding heuristics, but it omits the platform (WhatsApp) and the specific behavior change (an early-return guard that blocks non-self messages)." - }, - "usage": { - "input_tokens": 1360, - "output_tokens": 816 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "With groupPolicy set to \"allowlist\", group messages from senders not in groupAllowFrom are dropped at access-control and never reach the mention-gating/context-storage logic, so agents cannot see non-allowlisted group messages as context even when docs describe storing them for context-only.", - "solution_summary": "Introduce an opt-in config flag groupContextFromAll (top-level and per-account, default false) that causes non-allowlisted group messages to be marked storeForContext/contextOnly and saved to pending group history (no replies or read receipts), plus related type/schema and pipeline plumbing changes.", - "maintainer_signal_summary": "PR is opt-in and backward compatible by default, locally tested and small-scope, but Greptile flags a 3/5 confidence due to potential allowlist normalization mismatches and bypassing of group gating/state updates; labels include channel: whatsapp-web, stale, size: S.", - "dedupe_summary": "Likely overlaps with prior issues about group allowlist behavior, mention-gating, and context-storage; key de-dupe targets are any open issues on allowlist/normalization or 'store group messages for context' behavior, and this PR should be reconciled with discussions about applyGroupGating side effects." - }, - "judge": { - "score": 2, - "has_boilerplate": false, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "The summary is vague and meta — it references related topics (allowlist, mention-gating, context-storage) but fails to state the core change (store non-allowlisted group messages as pending context) and includes noisy guidance about dedupe targets rather than a concise technical problem description." - }, - "usage": { - "input_tokens": 1377, - "output_tokens": 709 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "sessions_spawn previously always created a new random-UUID session for each sub-agent spawn, preventing reuse of conversation history and allowing session keys to bypass allowlist/security checks.", - "solution_summary": "Introduce an optional sessionKey param: short keys map to agent:{agentId}:subagent:{key}, fully-qualified keys are validated to match targetAgentId, and omission falls back to crypto.randomUUID(); add four tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch rejection.", - "maintainer_signal_summary": "Reviewer flagged a blocking test compile error (importing non-existent openclaw-tools.js instead of clawdbot-tools.js), a case-sensitivity bug in fully-qualified key validation (embedded agentId not normalized), and inconsistent error handling (throw vs jsonResult); confidence low (2/5) and tests/files need fixes before merge.", - "dedupe_summary": "The test import looks like a typo causing a non-compiling duplicate test path; fix the import/name to align with existing clawdbot tests to remove redundancy and restore test coverage." - }, - "judge": { - "score": 1, - "has_boilerplate": false, - "signal_density": 1, - "would_cluster_correctly": false, - "reasoning": "The summary describes a test import typo and duplicate test path unrelated to the PR's core change (adding a sessionKey for deterministic sub-agent sessions), so it would mislead clustering and fail to match similar items." - }, - "usage": { - "input_tokens": 1430, - "output_tokens": 742 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) via the generic message(edit) flow called api.editMessageText(), causing Telegram to return 400 \"there is no text in the message to edit\" and leaving edits failing and UI inconsistent.", - "solution_summary": "editMessageTelegram now catches the specific 400 error (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption for captionable media; buttons-only updates call editMessageReplyMarkup directly; HTML parse-mode fallback and MESSAGE_NOT_MODIFIED handling preserved; six unit tests added to cover the paths.", - "maintainer_signal_summary": "Labels: channel: telegram, stale, size: M. Change is limited to src/telegram/send.ts, tests pass (510/510) with 6 new tests, Greptile confidence 5/5 and safe-to-merge signal from CI and test coverage.", - "dedupe_summary": "Backward-compatible fix with no interface or config changes and no linked/closing issue; risk noted that the error-regex may break if Telegram changes wording, but failures will gracefully propagate as before; revert by restoring src/telegram/send.ts if needed." - }, - "judge": { - "score": 2, - "has_boilerplate": false, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "The summary focuses on backwards-compatibility and risk/revert notes but omits the core technical change (auto-detecting captionable messages and retrying with editMessageCaption on a specific 400), so it lacks the specific signal needed to cluster similar Telegram edit/caption bugs." - }, - "usage": { - "input_tokens": 1311, - "output_tokens": 656 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "PR #24962 hardcoded disableBlockStreaming: true for WhatsApp to block reasoning/thinking leaks, which unintentionally disabled block streaming entirely so messages were batched and only sent when the agent finished—even when per-account or global block streaming was configured.", - "solution_summary": "The deliver callback now only suppresses payloads with kind === \"tool\" while reasoning payloads are filtered upstream by shouldSuppressReasoningPayload, and disableBlockStreaming is now driven by per-account channels.whatsapp.blockStreaming with fallback to agents.defaults.blockStreamingDefault; tests and live verification show progressive delivery works and no reasoning/tool blocks leak.", - "maintainer_signal_summary": "All 13 inbound contract tests pass and live WhatsApp testing confirms progressive text_end streaming when enabled; default behavior remains off unless explicitly enabled. Quick revert options: set channels.whatsapp.blockStreaming:false or remove blockStreamingDefault; primary files touched include extensions/whatsapp/src/auto-reply/monitor/process-message.ts and dispatch-from-config.ts. Original leak fixes (#24954, #24605) remain effective.", - "dedupe_summary": "Related: PR #24962 (introduced the regression) and issues #24954, #24605 (original leaks). Any open reports about WhatsApp streaming being disabled should be considered duplicates of this fix restoring config-driven block streaming." - }, - "judge": { - "score": 3, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Identifies that the PR fixes a WhatsApp block-streaming regression and links related reports, but omits the specific technical changes (restoring config-driven disableBlockStreaming and the deliver callback behavior) that would improve clustering accuracy." - }, - "usage": { - "input_tokens": 1303, - "output_tokens": 802 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Sending videos (larger than the old 5MB default) caused the Telegram gateway to hang because content-length checks prevented response bodies from being consumed and Agent.close() deadlocked, rendering the channel unresponsive until restart.", - "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher deadlock by adding a timeout and fallback destroy, made media download failures non-fatal, added timeouts/retries for getFile and downloads, and updated stripInboundMetadata to recognize inbound media paths.", - "maintainer_signal_summary": "Labeled as a bugfix and security hardening affecting gateway, integrations, and UI; includes tests (strip-inbound-meta.test.ts 15/15) and live verification, is backward-compatible and configurable, introduces no new dependencies and preserves existing SSRF policy.", - "dedupe_summary": "Related to inbound media paths commit 49196e024; not a duplicate of SSRF or storage changes and no other duplicate PRs referenced." - }, - "judge": { - "score": 1, - "has_boilerplate": true, - "signal_density": 1, - "would_cluster_correctly": false, - "reasoning": "The summary only references a commit hash and states it's not a duplicate of SSRF/storage changes, but fails to describe the core bug fixes or behavior (Telegram gateway hang, media download limits, timeout/retry fixes), so it provides almost no useful signal for clustering." - }, - "usage": { - "input_tokens": 1399, - "output_tokens": 744 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "Telegram inbound fallback body text used a hardcoded for non-audio media when no caption/text was present, causing documents (PDF, DOCX) and other non-photo media to be misidentified and the agent to respond incorrectly.", - "solution_summary": "Change resolveTelegramInboundBody to use the already-computed type-aware placeholder (e.g. , ) instead of hardcoded , and add unit tests verifying placeholders, media detection, and getFile behavior for document messages.", - "maintainer_signal_summary": "PR marked as a bug fix, closes #7116, limited scope (only agent-facing placeholder text), no security or permission changes, tests added and passing, reviewer conversations addressed with some unresolved items needing maintainer judgment.", - "dedupe_summary": "Root cause existed since original body computation (worked for photos but not other media); no duplicate fix noted in thread; new unit tests added to prevent regressions for document/media placeholder handling." - }, - "judge": { - "score": 3, - "has_boilerplate": true, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Captures root cause and that tests were added, but omits the key detail that the inbound fallback used a hardcoded and was changed to a type-aware placeholder, and includes an irrelevant comment about the thread." - }, - "usage": { - "input_tokens": 1226, - "output_tokens": 423 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "MEDIA_MAX_BYTES was hardcoded to 5 MB in src/media/store.ts so users could not raise the staging/upload limit, causing files larger than 5 MB (common PDFs, presentations, images) to fail to stage into sandboxed agents.", - "solution_summary": "Introduced a new configurable media.maxBytes (integer, optional, max 500 MB) read at gateway boot via get/set pattern, replaced hardcoded constant, updated error messages to reflect configured limit, preserved 5 MB default and kept deprecated constant for compatibility; unit tests pass.", - "maintainer_signal_summary": "PR closes #40880, addressed all bot review threads, passed 56 unit tests, is backward compatible with no required migration, and documents risks (disk usage) and mitigations (500 MB schema cap, TTL cleanup).", - "dedupe_summary": "Change is scoped to inbound media staging only; per-channel download limits and tools.media.* limits are unchanged, so no overlap with those configs; default behavior preserved and the deprecated constant retained to avoid breaking external consumers." - }, - "judge": { - "score": 3, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": false, - "reasoning": "The summary correctly states the scope and compatibility guarantees but omits the core actionable change (adding media.maxBytes and replacing the hardcoded MEDIA_MAX_BYTES), so it has useful signal but misses a key detail for accurate clustering." - }, - "usage": { - "input_tokens": 1347, - "output_tokens": 482 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "When Telegram media download fails (due to exceeding mediaMaxMb or Telegram API limits) the handler returned early and silently dropped the entire message, so the agent never saw the file or caption and the user received no ack or feedback.", - "solution_summary": "On download failure the code now extracts file metadata from the Telegram message, injects a metadata stub into msg.text (e.g. file name/size/type and reason), and lets the message continue through the normal pipeline so the agent can handle/respond; previous automatic user warning replies were removed.", - "maintainer_signal_summary": "This is marked a bugfix for the Telegram integration (labels: channel:telegram, size:M), verified via code/trace analysis but not live runtime tested; change is backward-compatible, revertable by a single commit, and linked to issues #24854 and #23452; risk of mutating msg.text is acknowledged but limited to the prior-error path.", - "dedupe_summary": "Related to issues #24854 and #23452 addressing the same silent-drop behavior; no other duplicates called out in the thread." - }, - "judge": { - "score": 1, - "has_boilerplate": true, - "signal_density": 1, - "would_cluster_correctly": false, - "reasoning": "The summary only references related issue numbers and 'silent-drop behavior' without describing the actual fix or symptoms, so it lacks the technical signal needed for deduplication." - }, - "usage": { - "input_tokens": 1189, - "output_tokens": 751 - } - }, - { - "thread_id": 5796, - "number": 24117, - "kind": "pull_request", - "title": "feat(cli): add --session-key targeting for openclaw agent", - "summary": { - "problem_summary": "openclaw agent lacked a --session-key option, forcing callers to rely on rotating UUID sessionId, sender-derived --to, or --agent main sessions, which causes CLI-triggered deliveries to resolve to the wrong session in multi-channel/multi-agent setups and breaks callback/automation routing.", - "solution_summary": "Adds a new --session-key CLI flag, threads sessionKey through AgentCliOpts into resolveSessionKeyForRequest and gateway parameters, updates help/validation text, and keeps resolution precedence as session-key -> session-id -> to -> agent; includes tests verifying parsing and forwarding.", - "maintainer_signal_summary": "Changes are additive, backward-compatible, and well-tested (two unit tests passing locally). Implementation touched register.agent.ts and agent-via-gateway; reviewers indicated low risk and high confidence for merging.", - "dedupe_summary": "This PR addresses recurring session-targeting reports (issues #22085, #13635, #12881) by providing an explicit stable targeting path but does not claim to fully resolve those broader reports; behavior for existing callers remains unchanged." - }, - "judge": { - "score": 2, - "has_boilerplate": true, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "Summary is vague and avoids the key technical change (adding --session-key to openclaw agent), instead referencing issue numbers and a generic “stable targeting path,” so it lacks the specific terms needed to cluster similar PRs." - }, - "usage": { - "input_tokens": 1161, - "output_tokens": 460 - } - }, - { - "thread_id": 766, - "number": 11611, - "kind": "pull_request", - "title": "feat: separate group-level allowlist from sender-level command authorization", - "summary": { - "problem_summary": "With groupPolicy set to \"allowlist\", inbound group interactions are currently gated by sender phone number (groupAllowFrom), which prevents allowing entire groups while letting any participant chat, restricts slash commands separately, and doesn’t stop the bot being added to spam groups.", - "solution_summary": "If groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, access control now gates by group JID membership (allowing anyone in an approved group to chat) while keeping slash-command authorization governed by groupAllowFrom; when no groups mapping is present, the legacy sender-based filtering is preserved.", - "maintainer_signal_summary": "Automated Greptile review rates the change 4/5 and considers it safe to merge; core logic change is localized to access-control and tests were added for the new behavior, with one noted stale inline comment in the code/documentation to fix.", - "dedupe_summary": "No duplicate behavior introduced; change preserves backward compatibility and depends on PR #3326 to avoid implicitly enabling group allowlist when groupPolicy is not explicitly \"allowlist\"." - }, - "judge": { - "score": 2, - "has_boilerplate": false, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "Summary focuses on backward-compatibility and a PR dependency but omits the core change—separating group-level allowlist from sender-level command authorization—so it lacks the necessary signal to cluster related issues." - }, - "usage": { - "input_tokens": 1211, - "output_tokens": 580 - } - }, - { - "thread_id": 17924, - "number": 46425, - "kind": "pull_request", - "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", - "summary": { - "problem_summary": "Configured telegramAccounts[].mediaMaxMb was not being applied to outbound reply messages with media because loadWebMedia in the reply delivery path always used the default 100 MB cap, so operators could not enforce lower limits for replies.", - "solution_summary": "Threaded mediaMaxBytes through the Telegram outbound reply delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path, added unit tests to verify maxBytes is passed or omitted, and kept direct-send and internals unchanged.", - "maintainer_signal_summary": "Includes two new unit tests and reports 108 related tests passing across delivery, dispatch, native-commands, and bot-message suites; build and checks were verified, reviewer comments addressed, and the PR closes #46023.", - "dedupe_summary": "This is a minimal parameter-threading fix that preserves existing defaults and direct-send behavior, avoids changing media-loading internals, and also patches the parallel native-commands path to prevent duplicated bugs." - }, - "judge": { - "score": 3, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": false, - "reasoning": "Summary correctly describes a parameter-threading fix and related paths but omits the specific Telegram/mediaMaxMb outbound-reply context and is a bit generic, so it may not reliably cluster with similar media-limit Telegram issues." - }, - "usage": { - "input_tokens": 1212, - "output_tokens": 493 - } - }, - { - "thread_id": 5712, - "number": 37050, - "kind": "pull_request", - "title": "fix(telegram): add HTML parse fallback for media captions", - "summary": { - "problem_summary": "Media sends with captions lacked the HTML-parse fallback that text messages had, so captions with invalid HTML entities caused Telegram API 400 errors and the entire media send to fail.", - "solution_summary": "Wrapped the media send callback in withTelegramHtmlParseFallback so on an HTML parse error the send is retried without parse_mode using the raw caption; applies to photo, document, video, animation, audio, voice and video note sends and includes a unit test verifying the retry.", - "maintainer_signal_summary": "Small, backward-compatible bugfix touching src/telegram/send.ts; adds one unit test (total 52 passing). Labeled channel: telegram, size: S; quick revert by restoring the send.ts change.", - "dedupe_summary": "This reuses the existing text-message HTML fallback behavior (no duplicate logic) and only alters the media send inner callback; edit-message, thread fallback and caption splitting logic remain unchanged." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise statement that media sends now reuse the text-message HTML fallback and only modify the media-send callback (leaving edit/thread/caption-splitting logic alone), but it omits the explicit failure case and retry-without-parse_mode detail." - }, - "usage": { - "input_tokens": 1086, - "output_tokens": 568 - } - }, - { - "thread_id": 21769, - "number": 55280, - "kind": "pull_request", - "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", - "summary": { - "problem_summary": "WhatsApp had block streaming effectively disabled (disableBlockStreaming hardcoded true) which batched intermediate text and introduced a delivery race: block replies were enqueued optimistically and the pipeline marked sentContentKeys before confirmed delivery, causing silent turns where neither blocks nor the final reply reached the user.", - "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming, fix the boolean activation bug, add blockStreamingCoalesceDefaults, implement dispatcher.sendBlockReplyAsync and await it in onBlockReply so sentContentKeys is set only after confirmed delivery, and update tests/mocks accordingly.", - "maintainer_signal_summary": "Tests updated and passing and manual checks verify both streaming-on and off behaviors; the PR fixes the root cause (not just a workaround) and includes a usage command (openclaw config set ...) making it ready for merge.", - "dedupe_summary": "Previously dedupe suppressed final replies because the pipeline optimistically marked blocks as sent; now the pipeline waits for confirmed block delivery before marking sentContentKeys, and falls back to the final reply if block delivery fails, preventing silent turns." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core delivery-race bug and its fix (waiting for confirmed block delivery and falling back to final reply) without boilerplate, so it will cluster with similar fixes." - }, - "usage": { - "input_tokens": 999, - "output_tokens": 772 - } - }, - { - "thread_id": 8098, - "number": 25543, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked group-level allowlist support, so administrators couldn't explicitly allow specific Signal groups by ID to bypass sender-level groupAllowFrom checks or run control commands.", - "solution_summary": "Adds channels.signal.groups. config with a SignalGroupConfig type and Zod schema, wires resolveChannelGroupPolicy into the Signal event handler to apply group-level bypasses for message delivery and command authorization, and updates docs; TypeScript compiles and manual testing was performed.", - "maintainer_signal_summary": "Introduces SignalGroupConfig (requireMention, tools, toolsBySender), integrates groupExplicitlyAllowed into commandGate authorizers, and uses hasGroupAllowFrom to maintain backward compatibility; follows patterns from other channels and is marked safe to merge by the reviewer.", - "dedupe_summary": "Supersedes PR #4337 and fixes #25540 by addressing Greptile review feedback (command gating and hasGroupAllowFrom handling); no other duplicate changes indicated." - }, - "judge": { - "score": 3, - "has_boilerplate": true, - "signal_density": 3, - "would_cluster_correctly": false, - "reasoning": "Mentions fixing command gating and hasGroupAllowFrom and references PR/issue numbers, but omits the key 'Signal' and the new groups config detail and includes PR/issue boilerplate, so it has some signal but misses important specifics for reliable clustering." - }, - "usage": { - "input_tokens": 941, - "output_tokens": 565 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal lacked group-level allowlisting via channels.signal.groups, so when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter and never matches group IDs.", - "solution_summary": "Added a groups config to SignalAccountSchemaBase, invoked resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom, and added four tests; all relevant test suites pass.", - "maintainer_signal_summary": "Small-size PR labeled channel: signal and fixes #25540. Fully tested: 624 config tests, 6 Signal monitor tests, and 17 group-policy tests (including 4 new) all pass. PR was AI-assisted with human oversight by JamesPeck.", - "dedupe_summary": "This change brings Signal into parity with other channels that already support a groups config (Telegram, WhatsApp, iMessage, IRC, BlueBubbles); it fixes a unique missing call in Signal rather than duplicating an existing fix." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise summary that Signal now supports a 'groups' config like other channels and fixes the missing group-level allowlist path, but it’s slightly vague about the exact schema/call added." - }, - "usage": { - "input_tokens": 741, - "output_tokens": 643 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "WhatsApp messages are delivered as a single batch because process-message.ts hardcodes disableBlockStreaming:true, so intermediate blocks and tool-call/sleep-delimited messages are not sent separately; there was also a bug treating queuedFinal as the only delivered marker.", - "solution_summary": "Respect channels.whatsapp.blockStreaming by adding resolveWhatsAppBlockStreamingEnabled and replacing the hardcoded flag, fix queuedFinal logic so intermediate block payloads count as replies, register blockStreaming capability and default in the WhatsApp dock, and update tests to cover enabled/disabled behavior.", - "maintainer_signal_summary": "Tests were updated and all 10 existing tests pass; tests now mock counts like other channels and verify anyReplyDelivered for intermediate blocks. Change is backward-compatible by default (blockStreaming false) and labeled channel: whatsapp-web, size: S.", - "dedupe_summary": "This change aligns WhatsApp with existing Telegram/Line blockStreaming behavior to avoid duplicate feature-requests; if blockStreaming is omitted it remains backward-compatible, so duplicate issues should reference this PR and the shared blockStreaming config." - }, - "judge": { - "score": 3, - "has_boilerplate": true, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Summarizes the core change (WhatsApp adopting Telegram/Line blockStreaming behavior and remaining backward-compatible) but includes meta/process noise about duplicate feature-requests and omits the important technical fix around queuedFinal/intermediate payload handling." - }, - "usage": { - "input_tokens": 491, - "output_tokens": 618 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v1-strip-boilerplate.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v1-strip-boilerplate.json deleted file mode 100644 index baf6af8..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v1-strip-boilerplate.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v1-strip-boilerplate", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "Summarize this GitHub issue or pull request. Your summary will be embedded as a vector and used for duplicate/similar-issue detection via clustering.\n\nIGNORE and strip away:\n- Issue/PR template sections (### Bug type, ### Steps to reproduce, ### Expected behavior, ### Checklist, etc.)\n- Testing instructions, QA notes, deployment steps\n- Checkbox lists and checklists\n- Version numbers, OS details, and environment info (unless they ARE the bug)\n- \"Related issues\" or \"References\" sections\n- Auto-generated content\n\nFOCUS on extracting:\n- The core problem or feature being described\n- Specific technical components, APIs, or subsystems affected\n- The root cause or proposed solution (if stated)\n\nReturn concise JSON with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Each field: plain text, no markdown, 1-3 sentences. The dedupe_summary should capture the essential topic in a way that would match other reports of the same issue.", - "timestamp": "2026-03-30T15:58:04.012Z", - "aggregate": { - "avg_score": 4.95, - "avg_signal_density": 4.95, - "boilerplate_count": 0, - "boilerplate_pct": 0, - "would_cluster_correctly_pct": 100, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 46623, - "total_output_tokens": 24277 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "channels.discord.proxy only applies to the Discord WebSocket Gateway connection; REST API calls during startup (deploy commands, fetch bot identity) and guild resolution do not use the configured HTTP proxy, causing fetch timeouts and guild channels to be non-functional behind a proxy while DMs still work.", - "solution_summary": "Ensure channels.discord.proxy (or a global proxy setting) configures an undici ProxyAgent or setGlobalDispatcher for all Discord REST requests and the guild-resolve code path during provider startup; consider adding retry logic for guild resolve.", - "maintainer_signal_summary": "Regression with reproduction steps and logs included: WebSocket connects via proxy but REST calls fail with \"fetch failed\" after ~10s. Workaround using NODE_OPTIONS to set a ProxyAgent fixes REST but guild resolve remains unresolved; standalone CLI resolve works when ProxyAgent is applied. Affected in proxy-required environments; suggested fix is to propagate proxy to undici dispatcher for all Discord HTTP calls.", - "dedupe_summary": "Discord channel proxy only proxies Gateway websocket, not REST API or guild resolve, causing startup REST failures and non-functional guild channels behind HTTP proxies." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that clearly states the core bug (proxying only the Gateway websocket, not REST/guild resolve) and the impact (startup REST failures and broken guild channels behind HTTP proxies)." - }, - "usage": { - "input_tokens": 2301, - "output_tokens": 491 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "Outbound Discord REST requests (sending messages/attachments) fail with \"fetch failed\" while the gateway WebSocket works via proxy. The channels.discord.proxy config is not applied to REST sends because the Carbon RequestClient uses fetch without proxy support, so REST calls bypass the configured proxy in restricted networks.", - "solution_summary": "Modify the Carbon RequestClient or its fetch usage to support proxies and honor channels.discord.proxy (e.g., use proxy-aware HTTP client or add HTTP/SOCKS/TUN proxy agent) so all REST requests, uploads and downloads route through the configured proxy.", - "maintainer_signal_summary": "User in a restricted region reproduces the issue with channels.discord.proxy set to http://127.0.0.1:7890/; logs show REST proxy enabled but outgoing REST requests still fail with TypeError: fetch failed. This prevents bots from sending messages and is high-impact for proxied environments.", - "dedupe_summary": "channels.discord.proxy not applied to Discord REST API calls — Carbon RequestClient/fetch lacks proxy support, causing outgoing message/attachment sends to fail behind proxies." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary stating that channels.discord.proxy isn't applied to REST calls because the Carbon RequestClient/fetch lacks proxy support, which directly explains the outgoing-send failures behind a proxy." - }, - "usage": { - "input_tokens": 983, - "output_tokens": 659 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "When OpenClaw is configured with an HTTP proxy for Discord, the Discord provider can log in and open the gateway but some startup REST calls (native command deployment and bot identity fetch) consistently fail with \"fetch failed\" while other Discord requests (application ID fetch, gateway) succeed.", - "solution_summary": "Investigate and fix inconsistent proxy usage in the Discord provider so all REST calls use the same proxy-aware fetch/dispatcher as the gateway/login path; update transport or dispatcher code paths to ensure proxy handling is applied uniformly for startup REST endpoints.", - "maintainer_signal_summary": "User provided logs, environment details (proxy configured via channels.discord.proxy), and repeated reproducible failures across restarts; offered to provide more environment details or test patches. The issue appears specific to OpenClaw's Discord provider rather than token/config errors.", - "dedupe_summary": "Discord provider behind HTTP proxy: gateway connects but some REST startup calls (deploy commands, fetch bot identity) fail with 'fetch failed' due to inconsistent proxy handling." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core failure (Discord gateway connects but REST startup calls like command deploy and bot identity fetch fail behind an HTTP proxy) without boilerplate or noise." - }, - "usage": { - "input_tokens": 1129, - "output_tokens": 482 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "OpenClaw logs show Discord proxy as enabled but all Discord REST calls fail with \"fetch failed\" and the gateway websocket never establishes under WSL2 using an HTTP proxy, despite curl via the same proxy succeeding. The Discord channel is unusable: REST deploy and identity fetches fail and gatewayConnected remains false.", - "solution_summary": "Ensure the configured HTTP proxy is actually applied to the Discord.js REST/fetch client and gateway (e.g., pass a proxy/agent to discord.js or honor HTTP(S)_PROXY environment variables) so REST requests and WebSocket connections are routed through the proxy.", - "maintainer_signal_summary": "Reproducible 100% under WSL2 with provided config (~/.openclaw/openclaw.json) and logs showing \"discord: rest proxy enabled\" but subsequent fetch failures; Feishu channel works and workaround is TUN-mode transparent proxy. Severity: high; affects any environment requiring an HTTP proxy for Discord.", - "dedupe_summary": "Discord proxy logged as enabled but discord.js REST fetch fails (fetch failed) and gateway websocket never connects under WSL2 — proxy not actually applied to HTTP client." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary captures the core bug that the Discord proxy is logged as enabled but REST requests fail because the HTTP client isn't using the proxy; it adds a possibly extraneous claim about the gateway websocket not connecting, so minor noise." - }, - "usage": { - "input_tokens": 966, - "output_tokens": 706 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "OpenClaw's Discord channel ignores the configured HTTP(S) proxy for REST API calls: incoming WebSocket messages work, but attempts to send messages fail with TypeError: fetch failed despite proxy set in channels.discord.proxy and HTTP(S)_PROXY env vars.", - "solution_summary": "Route Discord REST API calls through the same proxy-aware SSRF guard/shared fetch path used for web tools (web_search/web_fetch) so HTTP_PROXY/HTTPS_PROXY are respected by Discord REST requests.", - "maintainer_signal_summary": "Logs show \"discord final reply failed: TypeError: fetch failed\" while curl via the same proxy to the Discord API succeeds; Telegram and Discord WebSocket respect the proxy; changelog notes a proxy fix for web tools that did not include Discord REST calls.", - "dedupe_summary": "OpenClaw Discord REST API ignores configured HTTP(S) proxy, causing fetch failed errors when sending messages." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: states that OpenClaw's Discord REST client ignores the configured HTTP(S) proxy and results in fetch failures when sending messages, with no template noise." - }, - "usage": { - "input_tokens": 989, - "output_tokens": 592 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "The CLI command openclaw agent --channel discord --to resolves deliveries to agent:*:main instead of the live Discord channel/thread session because the session resolver does not search by delivery context (channel + to + accountId) and Discord targets are not normalized.", - "solution_summary": "A manual patch to compiled dist files added delivery-context-based session resolution, normalized Discord targets, fixed lookup logic in the compact resolver, and made the command path pass channel and replyAccount/accountId; recommended fix is to implement these changes in source and add revalidation tests.", - "maintainer_signal_summary": "High-regression-risk issue because the fix was applied to compiled dist files and will be lost on updates; maintainers should port the patch to source, add tests to cover delivery-context session lookup, and revalidate post-update behavior.", - "dedupe_summary": "openclaw CLI routes Discord targets to agent:*:main instead of resolving session by delivery context (channel+to+accountId)" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug—CLI routes Discord deliveries to agent:*:main instead of resolving sessions by delivery context (channel+to+accountId) with no extraneous boilerplate." - }, - "usage": { - "input_tokens": 916, - "output_tokens": 504 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions: (1) in Discord threads, volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) was being re-injected every turn causing progressive context bloat and degradation; (2) the CLI agent command resolver routed Discord deliveries to the default agent:*:main session instead of the live thread-bound session because it didn’t resolve by delivery context (channel+to+accountId). Affected runtime bundles and the gateway CLI session resolver.", - "solution_summary": "Patched compiled runtime bundles to stop injecting the volatile Discord metadata every turn, added session resolution by normalized delivery target in the gateway CLI, added delivery-context lookup in the agent command resolver, and passed channel and replyAccount/accountId into the resolver; also fixed an incorrect resolveAgentMainSessionKey usage. Verified by restarting the gateway service and observing correct writer/reasoning sessions and no repeated context.", - "maintainer_signal_summary": "Fixes were applied directly to compiled bundles in openclaw/dist/ (dist/pi-embedded-…, dist/gateway-cli-…, dist/compact-…, dist/register.agent-…), so changes are manual and will be lost on reinstall; maintainers should reapply or add a persistent patch/script post-update. Service restart confirmed resolution.", - "dedupe_summary": "Discord thread handling: volatile metadata re-injected every turn causing context bloat and CLI/gateway routing not resolving sessions by delivery context, causing commands to land in main instead of thread session." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, accurate summary that captures both core regressions (volatile metadata re-injection causing context bloat and incorrect session resolution for CLI commands) with no template noise, so it will cluster properly with similar issues." - }, - "usage": { - "input_tokens": 835, - "output_tokens": 591 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "The sandbox media staging cap is hardcoded to 5 MB in dist/store-*.js (const MEDIA_MAX_BYTES = 5 * 1024 * 1024), causing files larger than 5 MB (PDFs, PPTX, images) to be rejected or silently fail when copied into a sandboxed agent.", - "solution_summary": "Make the MEDIA_MAX_BYTES value configurable (for example via openclaw.json like tools.media.maxBytes) or otherwise derive it from existing per-type limits so the staging cap can be increased/overridden instead of being hardcoded.", - "maintainer_signal_summary": "Reporter marks this a regression present since at least v2026.3.7 and still in v2026.3.8, provides a manual sed patch workaround to alter dist/store-*.js, and requests adding configuration support for the staging cap.", - "dedupe_summary": "Hardcoded 5 MB sandbox media staging limit prevents uploading files >5MB; add tools.media.maxBytes config to override MEDIA_MAX_BYTES." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and technical: identifies the hardcoded MEDIA_MAX_BYTES 5MB sandbox staging cap and the desired tools.media.maxBytes config override without extraneous boilerplate." - }, - "usage": { - "input_tokens": 882, - "output_tokens": 617 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "Discord provider cannot send REST API requests: every REST call fails with a TypeError 'fetch failed' even though the Discord rest proxy is configured and logs show 'rest proxy enabled'. The Gateway WebSocket works and bot receives messages, and curl requests through the same proxy succeed, so only OpenClaw's Discord REST interactions are broken.", - "solution_summary": "No solution or workaround is provided in the report; the user supplied configuration and logs but did not identify a root cause. The issue appears to be a bug in OpenClaw's Discord REST/proxy integration (root cause not determined).", - "maintainer_signal_summary": "Reporter provided config, gateway logs, and reproduction evidence (curl works, Telegram unaffected) and requests a fix; severity unspecified but reproducible for Discord REST operations. Logs show 'fetch failed' errors during command deployment and identity fetch, indicating the REST path is failing despite proxy being enabled.", - "dedupe_summary": "Discord REST API requests in OpenClaw fail with 'fetch failed' when using a configured proxy, while Gateway WebSocket works and direct curl requests via the same proxy succeed." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary that captures the core bug (Discord REST requests in OpenClaw failing with 'fetch failed' despite proxy configuration, while WebSocket and curl via the same proxy work), which is the right signal for deduplication." - }, - "usage": { - "input_tokens": 827, - "output_tokens": 693 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "Discord REST API calls made by @buape/carbon's RequestClient ignore the channels.discord.proxy setting in openclaw.json, so REST requests (and related fetches like memorySearch and attachment downloads) bypass the configured proxy while the WebSocket gateway respects it. The RequestClient calls globalThis.fetch without a proxy-aware dispatcher and Node.js native fetch does not use HTTP_PROXY/HTTPS_PROXY unless special flags or a patched dispatcher are used.", - "solution_summary": "Have @buape/carbon's REST client accept an optional dispatcher or proxy configuration and have OpenClaw pass channels.discord.proxy to it so REST calls use the same proxy as the WebSocket. Short-term workarounds include setting a global undici dispatcher via a preload script (EnvHttpProxyAgent) rather than using NODE_OPTIONS=--use-env-proxy which breaks WebSockets.", - "maintainer_signal_summary": "Reporter asks maintainers to update the carbon RequestClient to accept and use a dispatcher/proxy and for OpenClaw to forward channels.discord.proxy. They provide a safe workaround (preloading a global EnvHttpProxyAgent) and warn that using --use-env-proxy breaks WebSocket connections.", - "dedupe_summary": "REST API requests via @buape/carbon ignore channels.discord.proxy because RequestClient uses globalThis.fetch without a proxy dispatcher, causing REST calls and media fetches to bypass configured proxy while WebSocket uses it." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core bug: RequestClient uses globalThis.fetch and ignores channels.discord.proxy so REST/media requests bypass the configured proxy while the gateway uses it." - }, - "usage": { - "input_tokens": 836, - "output_tokens": 582 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "Under sustained load with long-running conversations OpenClaw suffers performance degradation from unbounded context token growth (writer sessions ~42k tokens, reasoning ~90k) that increases per-turn latency, Discord lane blocking that causes serialization under load, and typing indicators getting stuck until TTL. Root causes identified are lack of effective context token limits, overly high concurrency saturating lanes, and insufficient event queue timeouts.", - "solution_summary": "Applied configuration changes to reduce concurrency (maxConcurrentSessions, agents.defaults.maxConcurrent, subagents limits), set agents.defaults.contextTokens to 80000, and increased channels.discord.eventQueue.listenerTimeout; restarted the gateway and verified no new \"lane wait exceeded\" or \"typing TTL reached\" errors. Also improved an anti-regression patching script to cover multiple hash variants and recommended making limits adaptive or configurable via openclaw config.", - "maintainer_signal_summary": "Maintainers lowered concurrency limits, added a context token cap, extended the Discord event listener timeout, restarted the gateway, and confirmed absence of new lane/typing errors post-restart, while noting restarts alone don’t fully eliminate cases under sustained load. They also hardened the patching script and suggested exposing/adapting limits rather than manual JSON edits.", - "dedupe_summary": "Performance degradation caused by unbounded context token growth and lane blocking under load, mitigated by capping context tokens, reducing concurrency, and increasing event queue timeouts." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core failure modes (unbounded context growth and lane blocking) and the applied mitigations, with no boilerplate or extraneous details, so it will cluster with similar reports." - }, - "usage": { - "input_tokens": 770, - "output_tokens": 698 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every conversation turn, causing context contamination, increased token usage, prompt cache busting, and performance degradation. The compiled runtime treats Discord like other platforms but lacks an isNewSession guard to limit injection to the first turn.", - "solution_summary": "Manual patches were applied to compiled files: dist/pi-embedded-D6PpOsxP.js to stop injecting the listed fields, and dist/compact-1mmJ_KWL.js to add an isNewSession guard and replace resolveAgentMainSessionKey with buildAgentMainSessionKey. Recommended follow-up is to apply the fix in source code and add a post-update revalidation script.", - "maintainer_signal_summary": "High regression risk because changes were made directly to compiled dist files and will be lost on update; local validation shows restored toolCall sessions and resolved prompt caching issues, but an official source-level patch is required and should be prioritized.", - "dedupe_summary": "Discord threads re-inject volatile metadata each turn (message_id, reply_to_id, timestamp, Thread starter, history), contaminating context and busting prompt cache." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core bug—Discord thread metadata is repeatedly injected each turn (fields listed), causing context pollution and prompt cache loss, with no extraneous boilerplate." - }, - "usage": { - "input_tokens": 782, - "output_tokens": 741 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord thread messages have volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) re-injected on every turn, causing context pollution, token bloat, degraded performance and prompt cache busting. The compiled runtime in dist/pi-embedded-D6PpOsxP.js is treating Discord threads as needing repeated volatile metadata, unlike Slack/iMessage fixes.", - "solution_summary": "A manual patch was applied to dist/pi-embedded-D6PpOsxP.js to avoid injecting those metadata fields after the first turn, restoring prompt caching and reducing token usage; recommended next steps are to implement the fix in source code and add a post-update revalidation script to avoid regression.", - "maintainer_signal_summary": "High regression risk because the change was made to compiled files in dist and will be lost on reinstall; requires an official source-level fix, tests/CI to prevent recurrence, and attention to related issues (#41355, #34228, #37047).", - "dedupe_summary": "Discord thread metadata re-injected every message (message_id, reply_to_id, timestamp, thread starter, chat history) causing context pollution and prompt cache busting due to a bug in compiled runtime dist/pi-embedded-D6PpOsxP.js." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that names the exact metadata re-injected, the impact (context pollution and cache busting), and the compiled runtime file causing it, with no boilerplate or noise." - }, - "usage": { - "input_tokens": 790, - "output_tokens": 809 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "Two regressions in Discord thread handling: repeated per-turn injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing unbounded context growth and latency in long threads, and the session resolver falling back to agent:*:main (wrong session) because CLI/resolver lookup did not consistently use delivery context (channel + to + accountId) and command path lacked sufficient context. The root cause was missing runtime patches in compiled dist/*.js bundles.", - "solution_summary": "Applied manual runtime patches to the affected dist/*.js bundles, restarted the gateway, created automatic backups and an idempotent re-application script, and validated that thread-bound sessions now resolve correctly; recommended non-regression tests (long-thread conversations, 15–30 minute monitoring, smoke tests for latency and context size).", - "maintainer_signal_summary": "Gateway restarted and is active; no new errors in logs post-restart; thread-bound sessions resolve correctly and CLI routing now targets the correct session. Note: fixes are manual edits to compiled bundles and must be re-applied after updates using the provided script and backups.", - "dedupe_summary": "Discord threads: per-turn metadata duplication grows context and causes latency, plus session resolver falls back to agent:*:main; patched compiled runtime bundles and added reapply script." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary that captures both core bugs (per-turn metadata growth causing context/latency and session resolver fallback to agent:*:main) and notes the runtime bundle patches and reapply script without extraneous boilerplate." - }, - "usage": { - "input_tokens": 766, - "output_tokens": 729 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "OpenClaw receives Discord messages and generates responses but fails to send replies; logs show \"final reply failed: TypeError: fetch failed\" even though Discord proxy is configured and reported as enabled.", - "solution_summary": "Likely a proxy-handling bug where the configured HTTP proxy is recognized for gateway/rest but not applied to the fetch used for sending reply messages; fix would ensure Discord REST fetch calls respect the configured proxy.", - "maintainer_signal_summary": "Regression that previously worked, reproducible in DM and group chats; logs show rest and gateway proxy enabled and a fetch failure, while curl through the same proxy succeeds, indicating the issue is in OpenClaw's proxy usage for reply sending.", - "dedupe_summary": "Discord replies fail with \"fetch failed\" because configured proxy is not being applied to message-sending HTTP requests." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (Discord message send fails with 'fetch failed' due to configured proxy not being applied) and contains no template noise." - }, - "usage": { - "input_tokens": 706, - "output_tokens": 754 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "In v2026.3.11 the CLI command openclaw agent --channel discord --to ... routes messages to the fallback session agent:*:main instead of the live Discord channel/thread session because the compiled session resolver in dist/gateway-cli-BjsM6fWb.js does not lookup sessions by delivery context and Discord targets are not normalized.", - "solution_summary": "A local patch to dist/gateway-cli-BjsM6fWb.js normalizes Discord targets (e.g. 148... → channel:148...), performs session lookup by delivery context (channel+to+accountId) before falling back to main, and thereby restores correct CLI routing to the live session.", - "maintainer_signal_summary": "High regression risk: this is a manual change to compiled files in dist/ and will be lost on updates; requires a source-level fix and tests to normalize targets and add delivery-context session lookup. Related requests: #36401 and #41562.", - "dedupe_summary": "CLI routing to Discord channels resolves to agent:*:main because session resolver omits delivery-context lookup and Discord target normalization; needs session lookup by channel+to+accountId." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core bug: CLI routes to agent:*:main because the session resolver lacks delivery-context lookup and Discord target normalization, and calls for lookup by channel+to+accountId." - }, - "usage": { - "input_tokens": 771, - "output_tokens": 721 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "After applying an earlier session-resolver patch in v2026.3.11, the compiled file dist/compact-1mmJ_KWL.js calls a non-existent function resolveAgentMainSessionKey, causing ReferenceError at runtime, agent command failures, and potential gateway crashes when processing agent commands.", - "solution_summary": "Locally patched dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with the correct buildAgentMainSessionKey(...), which removes the ReferenceError and restores agent command and session behavior; note this is a manual change to compiled output and will be lost on reinstall/update.", - "maintainer_signal_summary": "High-risk regression introduced by a prior fix; maintainers should apply the correct change in source/build (not just dist) and ensure fix #5 is included whenever fix #3 (session resolver in compact) is applied to avoid ReferenceError.", - "dedupe_summary": "Regression in dist/compact-1mmJ_KWL.js: ReferenceError due to resolveAgentMainSessionKey used instead of buildAgentMainSessionKey, causing agent command failures." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: names the compiled file, the incorrect vs correct function, and the resulting ReferenceError/agent failures, with no boilerplate—will cluster well with similar regressions." - }, - "usage": { - "input_tokens": 742, - "output_tokens": 485 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "Discord channel plugin fails health checks and message sends in proxy-required environments because channels.discord.proxy is only applied to WebSocket connections; REST API (HTTP) calls bypass the proxy and return \"fetch failed\" while curl with the same proxy succeeds.", - "solution_summary": "Apply channels.discord.proxy to the plugin's HTTP/REST client and health-check requests (or make the HTTP client respect the configured proxy), so REST API calls are routed through the proxy like the WebSocket connections.", - "maintainer_signal_summary": "User reports a regression (worked previously) on OpenClaw 2026.3.8 with reproduction steps and confirms the proxy itself works via curl; this impacts CLI message sending and health checks and needs a code change in the discord channel implementation.", - "dedupe_summary": "Discord channel REST API calls ignore channels.discord.proxy causing fetch failed, failing message send and health checks." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and focused on the core bug—Discord REST API calls ignore channels.discord.proxy causing fetch failures and related message/health-check errors—so it will cluster with similar reports." - }, - "usage": { - "input_tokens": 698, - "output_tokens": 497 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the session resolver in the agent command inside the compiled bundle dist/compact-1mmJ_KWL.js does not look up sessions by delivery context (channel + to + accountId), causing resolution to fall back to agent:*:main and agent responses to be associated with the wrong session.", - "solution_summary": "User patched dist/compact-1mmJ_KWL.js to add a delivery-context lookup: search for an existing session by channel+to+accountId and only fallback to main when no specific session is found; validation shows correct routing after the change.", - "maintainer_signal_summary": "The fix was applied manually to a compiled dist file and carries HIGH regression risk because it will be lost on reinstall/update; the lookup must be implemented in the source/build pipeline and shipped in an upstream release.", - "dedupe_summary": "Compiled bundle (dist/compact-1mmJ_KWL.js) session resolver missing delivery-context (channel+to+accountId) lookup, causing fallback to agent:*:main and incorrect session routing." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies the compiled file, the missing delivery-context (channel+to+accountId) lookup, and the resulting fallback to agent:*:main causing incorrect session routing." - }, - "usage": { - "input_tokens": 712, - "output_tokens": 678 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English (settings, labels, buttons, agent/skill pages, channel configs, status messages, tooltips), creating usability barriers for non-English users; hardcoded strings are present in the distributed control-ui bundle.", - "solution_summary": "Extract UI strings into a structured i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, provide a language selector or auto-detect system language, and build infrastructure to easily add more languages.", - "maintainer_signal_summary": "Requester flags this as medium-high priority for UX accessibility, notes docs already have zh-CN translations, and asks whether i18n for control-ui is on the roadmap or if maintainers/others want to collaborate on implementation.", - "dedupe_summary": "Control UI lacks localization; request to add i18n support and a Simplified Chinese (zh-CN) locale by extracting hardcoded English strings and adding language selection/auto-detect." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary that captures the core request to add i18n support and a zh-CN locale by extracting hardcoded English strings and adding language selection/auto-detect, with no extraneous boilerplate." - }, - "usage": { - "input_tokens": 633, - "output_tokens": 565 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "WhatsApp group chats could not apply channel-level systemPrompt instructions at account or group granularity, preventing per-account or per-group customization and wildcard defaults for groups without their own prompt.", - "solution_summary": "Adds systemPrompt support to WhatsApp config types/schema and implements resolution and composition logic so the final injected prompt is account systemPrompt plus the resolved group systemPrompt (specific group then '*' wildcard). Account-level groups fully override root groups; includes tests and docs.", - "maintainer_signal_summary": "Feature PR with tests and documentation; backward compatible and limited to the WhatsApp channel only (no hot-reload, allowlist, or mention-gating changes). Closes the linked issue and replaces a previous broader PR.", - "dedupe_summary": "Add per-account and per-group systemPrompt support for WhatsApp with precedence: account prompt then group prompt (specific -> wildcard); account groups override root groups." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that states the feature (per-account and per-group WhatsApp systemPrompt) and its precedence rules, matching the core change without boilerplate." - }, - "usage": { - "input_tokens": 2409, - "output_tokens": 545 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before inbound/agent processing, and group policy helpers resolved WhatsApp group policy without accountId so account-scoped groupAllowFrom/allowFrom were ignored, causing allowlist fallback to fail closed.", - "solution_summary": "Enable append processing only for WhatsApp group messages and propagate accountId when resolving WhatsApp group policy so account-scoped groupAllowFrom/allowFrom (and allowlist fallback) are applied; DM append history remains skipped.", - "maintainer_signal_summary": "Bugfix touching web inbound and group activation code (notably src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts), validated against a live OpenClaw WhatsApp deployment with trace logs and targeted tests; change is backward-compatible and scoped to WhatsApp groups.", - "dedupe_summary": "WhatsApp group append messages skipped and account-scoped group policy ignored, preventing group messages from triggering the agent." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it names the WhatsApp group 'append' handling being skipped and the account-scoped group policy being ignored, which are the core issues needed for correct deduplication." - }, - "usage": { - "input_tokens": 1961, - "output_tokens": 712 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because the gateway failed to resolve the client IP when a trusted proxy sent requests without forwarding headers and the local-request check did not fall back to the socket remote address when allowRealIpFallback was enabled. Additionally, unconditional acceptance of .ts.net Host headers posed a security concern.", - "solution_summary": "resolveClientIp now returns the socket address as a fallback when no forwarding headers are present; isLocalDirectRequest checks req.socket.remoteAddress when allowRealIpFallback is enabled; isLocalishHost now gates .ts.net acceptance behind the existing allowTailscale flag and that flag is threaded through relevant callers. Documentation updated and tests added to verify behavior.", - "maintainer_signal_summary": "New tests added (105 passing) and failing logs fixed; PR closes issue #28472 and depends on another PR for overlapping allowTailscale changes. No new config keys introduced, but docs require gateway.trustedProxies and gateway.allowRealIpFallback when using Tailscale Serve; allowTailscale still defaults to true for tailscale.mode=\"serve\" to preserve compatibility.", - "dedupe_summary": "401 authentication failure for Canvas behind Tailscale Serve due to missing client-IP fallback and local-request detection; fixed by falling back to socket.remoteAddress and gating .ts.net host acceptance behind allowTailscale." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core bug (401 for Canvas behind Tailscale Serve), root causes (missing client-IP/socket fallback and local-request detection), and the precise fixes (fallback to socket.remoteAddress and gating .ts.net via allowTailscale), so it will cluster correctly with similar reports." - }, - "usage": { - "input_tokens": 1800, - "output_tokens": 805 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "checkInboundAccessControl computes an isSelfChat value but never uses it as a guard, so non-self WhatsApp messages (groups, DMs from strangers, broadcasts) can pass through because resolveWhatsAppRuntimeGroupPolicy defaults to \"open\" when cfg.channels.whatsapp exists with no explicit groupPolicy. This violates the intended self-chat isolation boundary and lets non-self messages reach downstream processing.", - "solution_summary": "Add an early-return guard in checkInboundAccessControl that rejects any non-self message when account.selfChatMode === true, placed before group/DM policy evaluation. The guard checks the explicit config flag only (not the heuristic isSelfChat) to avoid blocking teammates allowed via allowFrom; isSelfChat remains computed for UI/logging.", - "maintainer_signal_summary": "Change limited to src/web/inbound/access-control.ts with tests updated/verified; all 9 related tests pass and code-path analysis confirms the guard fires before policy resolution. The fix is intentionally scoped to explicit selfChatMode and can be reverted by removing the early-return guard; reviewers should note auto-detected self-chat is not hard-blocked by design.", - "dedupe_summary": "WhatsApp self-chat mode not enforcing isolation because isSelfChat was computed but unused, allowing group/other non-self messages through; fixed by enforcing explicit selfChatMode guard in inbound access control." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the bug (isSelfChat computed but unused), its effect (non-self WhatsApp messages bypass isolation), and the fix (explicit selfChatMode guard in inbound access control)." - }, - "usage": { - "input_tokens": 1515, - "output_tokens": 583 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "When groupPolicy is set to \"allowlist\", group messages from senders not in groupAllowFrom are dropped at access-control and never reach the mention-gating/context-storage logic, so agents cannot see those messages in the \"Chat messages since your last reply\" context block.", - "solution_summary": "Adds an opt-in flag groupContextFromAll (top-level and per-account, default false). When enabled, access-control returns storeForContext for non-allowlisted senders, messages are marked contextOnly (no read receipts), stored as pending group history, and do not trigger replies; per-account config takes precedence.", - "maintainer_signal_summary": "PR is localized and opt-in with zero default behavior change and local verification with two bots; greptile flagged a 3/5 confidence due to potential allowlist normalization/matching bugs and the new context-only path bypassing existing group gating/state updates that may cause subtle inconsistencies.", - "dedupe_summary": "WhatsApp groups: non-allowlisted senders' messages are currently dropped under groupPolicy: allowlist; add groupContextFromAll flag to store those messages as contextOnly instead of discarding." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: states that with groupPolicy: allowlist non-allowlisted group messages are dropped and the change adds a groupContextFromAll flag to store them as context-only instead of discarding." - }, - "usage": { - "input_tokens": 1532, - "output_tokens": 562 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "Adds an optional sessionKey parameter to sessions_spawn so sub-agents can reuse deterministic sessions instead of creating a new random-UUID each spawn. Short keys are namespaced to agent:{agentId}:subagent:{key}, fully-qualified keys are accepted only if the embedded agentId matches the target agent, and omission falls back to crypto.randomUUID(). Also intended to fix a session key allowlist bypass and includes four unit tests for deterministic and fallback behaviors.", - "solution_summary": "Implements sessionKey handling, namespacing, and cross-agent validation in sessions_spawn, with a fallback to crypto.randomUUID() when sessionKey is omitted; adds tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch rejection. Note: tests currently reference a non-existent import which prevents compilation.", - "maintainer_signal_summary": "Automated review flagged blocking issues: a test imports ./openclaw-tools.js which does not exist (should be ./clawdbot-tools.js), a case-sensitivity bug where embedded agentId from fully-qualified keys isn’t normalized against normalizeAgentId(), and inconsistent error handling (throw vs returning jsonResult) for cross-agent mismatches; these must be fixed before merging.", - "dedupe_summary": "Add sessionKey to sessions_spawn to reuse deterministic sub-agent sessions with namespacing and cross-agent validation, plus tests (but test import, case-sensitivity, and error-handling issues reported)." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summarizes the core change—adding a deterministic sessionKey to sessions_spawn with namespacing and cross-agent validation—so it will cluster correctly, but it includes noisy test/import/case-sensitivity/error-handling remarks that are not essential for deduplication." - }, - "usage": { - "input_tokens": 1585, - "output_tokens": 677 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Editing Telegram messages with media (photo, video, audio, document, animation) using the generic edit path calls editMessageText and gets a 400 error \"there is no text in the message to edit\", causing message(action=edit) to fail for captionable messages and leave the UI inconsistent.", - "solution_summary": "editMessageTelegram in src/telegram/send.ts now detects that specific 400 error via NO_TEXT_IN_MESSAGE_RE, retries with editMessageCaption for captionable messages, and uses editMessageReplyMarkup directly for buttons-only updates; MESSAGE_NOT_MODIFIED is treated as success and HTML parse fallback is preserved.", - "maintainer_signal_summary": "Six unit tests added covering retry path, buttons-only path, error propagation, and MESSAGE_NOT_MODIFIED handling; change is backward-compatible with no API surface changes, touches only the Telegram adapter file, and carries a small risk if Telegram's error wording changes (regex scoped tightly); not manually end-to-end tested with a live bot.", - "dedupe_summary": "Telegram media messages fail to edit because editMessageText is used on captionable messages; fix is to auto-detect the \"no text in message\" 400 and retry with editMessageCaption or use editMessageReplyMarkup for buttons-only edits." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific description of the Telegram editMessageText->400 failure on captionable media and the retry-to-editMessageCaption / use editMessageReplyMarkup fix, which will match similar API-edit bugs." - }, - "usage": { - "input_tokens": 1466, - "output_tokens": 547 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "A prior change hardcoded disableBlockStreaming: true for WhatsApp, which unintentionally turned off block streaming so messages were batched and sent only when the agent finished, ignoring per-account or global blockStreaming settings.", - "solution_summary": "Restore config-driven behavior: the deliver callback now only suppresses tool payloads (not all payloads) and disableBlockStreaming is derived from channels.whatsapp.blockStreaming or agents.defaults.blockStreamingDefault, while upstream filtering continues to suppress reasoning payloads.", - "maintainer_signal_summary": "Fix verified with unit/integration tests and live WhatsApp checks; 13 inbound contract tests pass; backward compatible and no security/network changes; risk noted if upstream shouldSuppressReasoningPayload is removed.", - "dedupe_summary": "WhatsApp block streaming was disabled by a hardcoded flag; this PR makes block streaming respect per-account and global config and only suppresses tool/reasoning payloads appropriately." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change—removing a hardcoded disable for WhatsApp block streaming and making suppression limited to tool/reasoning payloads while honoring per-account/global config—without extraneous boilerplate." - }, - "usage": { - "input_tokens": 1458, - "output_tokens": 364 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Sending video media via the Telegram channel could make the gateway hang and become unresponsive because a content-length check failed before the response body was consumed, causing undici Agent.close() (closeDispatcher) to deadlock; the old default mediaMaxMb (5MB) also caused otherwise-valid files to be rejected.", - "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher to timeout and fall back to destroy to avoid Agent.close deadlocks, made media download errors non-fatal so captions still forward, and added timeouts and retry logic to getFile and file downloads; also extended inbound metadata stripping for UI.", - "maintainer_signal_summary": "Changes are backward compatible, no new dependencies, and configurable via channels.telegram.mediaMaxMb; tests updated (strip-inbound-meta.test.ts passing), verified end-to-end with a 7.5MB video, and key files touched include src/infra/net/ssrf.ts and src/telegram/bot.ts; risk noted for the 3s dispatcher timeout but mitigations are documented.", - "dedupe_summary": "Telegram gateway hangs on video uploads due to unconsumed response body causing undici Agent.close deadlock and strict 5MB media limit; fix increases media limit and adds dispatcher timeout/retries to prevent hangs." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary capturing the root cause (undici Agent.close deadlock from unconsumed response body), symptom (gateway hangs on video uploads), and the key fixes (increase media limit, add timeouts/retries), with no boilerplate noise." - }, - "usage": { - "input_tokens": 1554, - "output_tokens": 762 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "Telegram inbound body fallback always used the hardcoded \"\" when a message had media but no caption, so document, video, and other non-photo attachments were misidentified to the agent (e.g., PDFs appeared as images).", - "solution_summary": "Change resolveTelegramInboundBody to use the already-computed type-aware placeholder variable (producing , , etc.) for the fallback body text and add unit tests to cover media placeholder behavior and document message handling; media download and extraction logic remain unchanged.", - "maintainer_signal_summary": "This is a small, scoped bugfix in the Telegram integration that updates only the agent-facing body placeholder; new unit tests were added in extensions/telegram/src/bot/helpers.test.ts and extensions/telegram/src/bot.create-telegram-bot.test.ts to prevent regressions; closes #7116.", - "dedupe_summary": "Telegram integration mislabels non-photo media as in empty-body fallback instead of using a type-aware / placeholder." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific description of the Telegram integration bug (wrong fallback vs type-aware /), with no boilerplate—suitable for deduplication." - }, - "usage": { - "input_tokens": 1381, - "output_tokens": 450 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "MEDIA_MAX_BYTES was hardcoded to 5 MB in src/media/store.ts so files larger than 5 MB silently failed to stage into sandboxed agents, preventing common file types (PDFs, presentations, high-res images) from being processed.", - "solution_summary": "Introduced a new media.maxBytes configuration field, replaced the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes() accessors, and wired gateway boot to read the config (defaulting to 5 MB). Schema caps values at 500 MB and error messages now show the actual configured limit.", - "maintainer_signal_summary": "All 56 unit tests pass (store, server, staging, schema tests); bot review comments were addressed (reset behavior, MB formatting); change is backward compatible (default preserved, deprecated constant exported) and reversible by removing the config key; risks mitigated by schema max and TTL cleanup.", - "dedupe_summary": "Make media staging size limit configurable via media.maxBytes (override default 5 MB) to allow staging of larger files into sandboxed agents." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and focused: states the core change (make media.maxBytes configurable replacing hardcoded 5 MB limit) using specific technical terms and no template noise." - }, - "usage": { - "input_tokens": 1502, - "output_tokens": 477 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "When Telegram media download fails (due to exceeding configured mediaMaxMb or Telegram API errors), the handler returned early and silently dropped the entire message so the agent never saw it, received no ack reaction, and the user got no indication. This affects the Telegram integration message pipeline in src/telegram/bot-handlers.ts and processMessage dispatching.", - "solution_summary": "On download failure, extract file metadata (name, size, type) from the Telegram message object, build a metadata stub and inject it into msg.text, and let the message continue through the normal pipeline instead of returning early; previous automatic bot replies for these errors are removed so the agent can handle responses.", - "maintainer_signal_summary": "Bug fix targeting the Telegram integration (src/telegram/bot-handlers.ts). Backward compatible, low risk, reversible by reverting the single commit; note the change mutates msg.text only in the error path where messages previously were dropped.", - "dedupe_summary": "Telegram media download failures cause silent message drops; inject file metadata into message text and continue processing to avoid loss." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies the root cause (silent drops when Telegram media download fails) and the implemented fix (inject file metadata into message text and let the message continue), which is ideal for deduplication." - }, - "usage": { - "input_tokens": 1344, - "output_tokens": 479 - } - }, - { - "thread_id": 5796, - "number": 24117, - "kind": "pull_request", - "title": "feat(cli): add --session-key targeting for openclaw agent", - "summary": { - "problem_summary": "The openclaw agent CLI lacked a --session-key option, so CLI-triggered deliveries in multi-channel/multi-agent setups could resolve to the agent main session instead of an intended stable channel session because sessionId is UUID-based and external callers often can only supply a stable session key or lack a sender-derived --to.", - "solution_summary": "Adds a first-class --session-key flag to the openclaw agent command, surfaces sessionKey in AgentCliOpts and forwards it to resolveSessionKeyForRequest/gateway, updates help/validation text, keeps resolution precedence (sessionKey > sessionId > to > agent), and adds tests verifying CLI parsing and gateway forwarding.", - "maintainer_signal_summary": "Tests added for CLI parsing and gateway forwarding and are passing; change is additive and backward-compatible, small risk, follows existing resolver semantics and ready to merge.", - "dedupe_summary": "Add explicit --session-key support to openclaw agent CLI to target stable session keys for message/callback routing." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: states the CLI change (--session-key), target command (openclaw agent), and intent (use stable session keys for message/callback routing), with no boilerplate or noisy details." - }, - "usage": { - "input_tokens": 1316, - "output_tokens": 571 - } - }, - { - "thread_id": 766, - "number": 11611, - "kind": "pull_request", - "title": "feat: separate group-level allowlist from sender-level command authorization", - "summary": { - "problem_summary": "With groupPolicy: \"allowlist\" the current logic gates all group interactions (chat and slash commands) by sender phone number via groupAllowFrom, making it impossible to allowlist groups by JID, keep chat open to all participants in approved groups, or restrict slash commands to owners while preventing spam groups.", - "solution_summary": "When groupPolicy: \"allowlist\" and a channels.whatsapp.groups mapping exists, the access control now uses the groups mapping (group JIDs) as the group-level gate so approved groups accept messages from any participant while groupAllowFrom still controls slash command authorization; when no groups mapping is present, the legacy sender-based behavior is preserved.", - "maintainer_signal_summary": "Code changes update src/web/inbound/access-control.ts to call resolveChannelGroupPolicy, update JSDoc in src/config/types.whatsapp.ts, and add a Vitest suite (4 tests) covering allowlist behavior; this PR depends on #3326 and preserves backward compatibility, with one minor stale-comment doc fix noted.", - "dedupe_summary": "Separate group-level allowlist by group JID from sender-level command authorization: when a groups mapping exists, gate group membership by JID and keep slash commands restricted by groupAllowFrom." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core change—use groups JID mapping for group-level allowlist while preserving sender-based command authorization via groupAllowFrom—without noisy boilerplate." - }, - "usage": { - "input_tokens": 1366, - "output_tokens": 582 - } - }, - { - "thread_id": 17924, - "number": 46425, - "kind": "pull_request", - "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", - "summary": { - "problem_summary": "Configured mediaMaxMb for Telegram is ignored when delivering final reply messages containing media because loadWebMedia in the reply delivery path receives no maxBytes and falls back to the 100 MB default, while the direct-send path already honored the setting.", - "solution_summary": "Thread mediaMaxBytes through the Telegram outbound reply delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts and bot-native-commands.ts) so loadWebMedia receives and enforces the configured maxBytes; added unit tests verifying behavior and preserved default when unset.", - "maintainer_signal_summary": "Change is a small, backward-compatible bug fix limited to the Telegram integration, includes two unit tests and all related suites passing (108 tests), no security or API changes, and closes the linked issue.", - "dedupe_summary": "Telegram outbound reply media ignores mediaMaxMb (loadWebMedia lacked maxBytes); fix by passing mediaMaxBytes through outbound delivery chain to enforce limit." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that states the bug (outbound replies ignore mediaMaxMb), root cause (loadWebMedia lacked maxBytes), and the fix (thread mediaMaxBytes through delivery chain), making it ideal for deduplication." - }, - "usage": { - "input_tokens": 1367, - "output_tokens": 443 - } - }, - { - "thread_id": 5712, - "number": 37050, - "kind": "pull_request", - "title": "fix(telegram): add HTML parse fallback for media captions", - "summary": { - "problem_summary": "Telegram media sends (photo, document, video, animation, audio, voice, video note) could fail with a 400 \"can't parse entities\" error when captions contained invalid HTML entities because the media send path did not use the HTML-parse fallback used by text messages.", - "solution_summary": "Wrap the media send callback in withTelegramHtmlParseFallback so that on an HTML parse error the send is retried without parse_mode and with the raw caption (plain text); this change applies to all media types and leaves caption-splitting and other logic unchanged.", - "maintainer_signal_summary": "Small, backward-compatible bug fix in src/telegram/send.ts with a new unit test verifying the retry behavior; no config changes, text/edit paths unaffected, and the change can be reverted by restoring the send.ts modification.", - "dedupe_summary": "Telegram media caption HTML-parse errors cause 400 failures; add fallback to retry media sends as plain text without parse_mode." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies HTML parse errors in Telegram media captions causing 400 failures and the added fallback retrying sends as plain-text without parse_mode, with no irrelevant boilerplate." - }, - "usage": { - "input_tokens": 1241, - "output_tokens": 706 - } - }, - { - "thread_id": 21769, - "number": 55280, - "kind": "pull_request", - "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", - "summary": { - "problem_summary": "WhatsApp had block streaming effectively disabled and a delivery race: block replies were enqueued optimistically and the pipeline marked content as sent before actual delivery, which could cause neither intermediate blocks nor the final reply to be delivered. A ternary bug also caused configured blockStreaming to be ignored (undefined vs false).", - "solution_summary": "Expose block streaming as channels.whatsapp.blockStreaming, fix process-message to correctly resolve the config, add sensible coalescing defaults, implement sendBlockReplyAsync to await confirmed delivery, and update onBlockReply to await that promise so sentContentKeys are only marked after success; tests updated accordingly.", - "maintainer_signal_summary": "Changes touch process-message.ts, reply-dispatcher.ts, dispatch-from-config.ts, shared.ts and tests; includes unit and manual verification showing both streaming-on and off behavior, and explicitly fixes the root cause rather than the prior workaround.", - "dedupe_summary": "WhatsApp block-streaming disabled/hardcoded and delivery race where optimistic enqueue suppressed final replies; needs configurable streaming and confirm-on-delivery for block replies." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific summary that captures the core change (make WhatsApp block-streaming configurable) and the root cause (delivery race/optimistic enqueue suppressing final replies) without boilerplate, so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 1154, - "output_tokens": 742 - } - }, - { - "thread_id": 8098, - "number": 25543, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked a group-level allowlist option, so administrators could not whitelist specific Signal groups by ID to bypass sender-level groupAllowFrom checks or allow groups to run control commands, unlike other channels (Telegram, WhatsApp, etc.).", - "solution_summary": "Add channels.signal.groups. config with a SignalGroupConfig type and Zod schema, wire resolveChannelGroupPolicy into the Signal event handler to grant group-level bypass of sender-level checks, and incorporate groupExplicitlyAllowed into commandGate authorizers; update docs and types accordingly.", - "maintainer_signal_summary": "PR is safe to merge per review: TypeScript compilation passes, manually tested on a live Signal instance, addresses Greptile feedback by including group-level authorization in command gating, and preserves backward compatibility via hasGroupAllowFrom handling (confidence reported 5/5).", - "dedupe_summary": "Add group-level allowlist support for Signal (channels.signal.groups.) to bypass sender-level groupAllowFrom checks and permit control commands for explicitly allowed groups." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states the core change (new channels.signal.groups. allowlist), its effect (bypass sender-level groupAllowFrom) and purpose (permit control commands for allowed groups), with no boilerplate or noise." - }, - "usage": { - "input_tokens": 1096, - "output_tokens": 530 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" and groupAllowFrom listed group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter and never matches group IDs.", - "solution_summary": "Add a groups config to SignalAccountSchemaBase (same shape as other channels) and call resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom; add four tests to cover group policy resolution.", - "maintainer_signal_summary": "Small, targeted fix (label: channel: signal, size: S) that adds schema and event-handler logic; all relevant tests (config, Signal monitor, group-policy) pass and four new tests added; fixes #25540.", - "dedupe_summary": "Signal missing groups config / resolveChannelGroupPolicy caused allowlist group messages to be dropped; add group-level allowlist support." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise technical summary that identifies Signal lacked a groups config causing group-level allowlist checks to fail and states the fix to add group-level allowlist support." - }, - "usage": { - "input_tokens": 896, - "output_tokens": 539 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "WhatsApp channel hardcodes disableBlockStreaming:true in process-message.ts, so when agents send multiple messages with tool calls/sleeps in between all text is coalesced into one final WhatsApp message; delivery logic also incorrectly treated only queuedFinal as marking a reply delivered, ignoring intermediate blocks.", - "solution_summary": "Make WhatsApp honor channels.whatsapp.blockStreaming by adding resolveWhatsAppBlockStreamingEnabled and replacing the hardcoded flag with the config value, fix delivery counting so intermediate blocks can count as delivered, register blockStreaming capability and default in dock, and update tests accordingly.", - "maintainer_signal_summary": "Small, backward-compatible change with updated tests that aligns WhatsApp behavior with Telegram/Line; labeled for whatsapp-web, web-ui and agents and ready for review.", - "dedupe_summary": "WhatsApp always coalesces message blocks due to hardcoded disableBlockStreaming; change to config-driven blockStreaming so intermediate blocks are delivered separately." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it identifies the hardcoded disableBlockStreaming causing WhatsApp to coalesce message blocks and states the change to make block streaming config-driven so intermediate blocks are delivered separately." - }, - "usage": { - "input_tokens": 646, - "output_tokens": 607 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v10-minimal-rules.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v10-minimal-rules.json deleted file mode 100644 index 09f15fa..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v10-minimal-rules.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v10-minimal-rules", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "Summarize this GitHub issue/PR for duplicate detection. Strip template boilerplate and testing notes. Focus on the specific technical problem or change.\n\nReturn JSON: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Plain text, 1-3 sentences each.", - "timestamp": "2026-03-30T16:14:26.454Z", - "aggregate": { - "avg_score": 4.1, - "avg_signal_density": 4.03, - "boilerplate_count": 10, - "boilerplate_pct": 25, - "would_cluster_correctly_pct": 95, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 40703, - "total_output_tokens": 25497 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "channels.discord.proxy only proxies the Discord WebSocket Gateway connection; startup REST API requests (command deployment, fetch-bot-identity) and guild resolution do not use the configured HTTP proxy, causing REST calls to time out with \"fetch failed\" and guild channels to be unresolved and non-functional behind an HTTP proxy (DMs via WebSocket still work).", - "solution_summary": "Ensure the configured channels.discord.proxy (or a new global proxy setting) is applied to the undici dispatcher used for all REST API calls and guild-resolve code paths (not just the WebSocket), e.g., install a ProxyAgent globally or pass a proxied dispatcher during Discord provider startup; add retry handling for guild resolve. ", - "maintainer_signal_summary": "Regression report with OpenClaw 2026.3.13, Node 25.8.0 (undici fetch), reproducible behind corporate proxy; user provides logs, repro steps, and a partial workaround (NODE_OPTIONS + ProxyAgent fixes REST but not guild resolve). Labels: bug, regression — needs fix to make proxy support complete. ", - "dedupe_summary": "Fingerprint: Discord channel proxy regression — websocket proxied but undici/REST and guild-resolve not proxied, causing \"fetch failed\" and \"channels unresolved: guild:\"; OpenClaw 2026.3.13, Node 25/undici; workaround using global ProxyAgent partially succeeds (REST) but guild resolve still fails." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Clear, specific description of the core regression (WebSocket proxied but undici/REST and guild-resolve not), but it includes extraneous version and workaround details that aren't needed for deduplication." - }, - "usage": { - "input_tokens": 2153, - "output_tokens": 618 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "Outbound Discord REST requests (message sends and attachment transfers) do not honor channels.discord.proxy; gateway/WebSocket connections work via the proxy but RequestClient.fetch has no proxy support, producing “fetch failed” errors in restricted-network environments.", - "solution_summary": "Implement proxy support in the RequestClient.fetch path and ensure all REST/media requests use channels.discord.proxy (support HTTP/SOCKS/TUN). Concretely: inject an appropriate HTTP/SOCKS agent or use a proxy-aware fetch wrapper that reads the configured proxy URL and is applied to message send, attachment upload/download, and other REST calls.", - "maintainer_signal_summary": "Reporter filed a bug on v2026.2.26 for users in China and labeled it bug; there is no maintainer response in the thread and the report requests prioritization to restore functionality in restricted regions.", - "dedupe_summary": "Check for existing issues named or tagged: 'discord proxy', 'channels.discord.proxy not applied', 'RequestClient fetch proxy', 'Discord REST proxy', or 'fetch failed proxy'. If any match, mark duplicates of the RequestClient/REST proxy gap." - }, - "judge": { - "score": 3, - "has_boilerplate": true, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Includes the right technical keywords (Discord REST proxy, channels.discord.proxy, RequestClient/fetch) that will match duplicates but is phrased as triage instructions rather than a concise problem statement, so it's useful but noisy." - }, - "usage": { - "input_tokens": 835, - "output_tokens": 804 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "When OpenClaw’s Discord provider runs behind an HTTP proxy, the gateway/login path succeeds but some REST startup calls consistently fail with “fetch failed” (notably native command deployment and bot identity fetch). The symptoms show proxy is enabled and applicationId fetch works, but deploy-rest and fetch-bot-identity requests fail while the WebSocket/gateway remains connected.", - "solution_summary": "Ensure all Discord REST requests use the same proxy-aware HTTP client/dispatcher as the gateway/login path—propagate channels.discord.proxy into every REST call path or unify to a single HTTP client implementation; add targeted logging and retries for startup REST calls to surface proxy misconfiguration and transient errors.", - "maintainer_signal_summary": "Reproducible on OpenClaw 2026.3.12 (service touched by 2026.3.13) with channels.discord.proxy set; likely a bug where some REST request paths don’t pick up proxy settings. This should be investigated by checking where the proxy setting is applied and harmonizing the fetch/dispatcher usage across startup REST flows.", - "dedupe_summary": "Search existing issues for keywords: \"fetch failed\", \"discord rest proxy\", \"deploy-rest:put:error\", \"fetch-bot-identity\", \"channels.discord.proxy\", and \"stale-socket\"; include OpenClaw versions 2026.3.12/2026.3.13 and sample log lines showing deploy-rest:put:error and logged in to discord. " - }, - "judge": { - "score": 2, - "has_boilerplate": true, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "The summary is framed as search/instructional text with versions and sample logs instead of a concise statement of the core problem (Discord REST calls like deploy/identity failing behind an HTTP proxy), so it contains noise and won't reliably cluster duplicates." - }, - "usage": { - "input_tokens": 981, - "output_tokens": 670 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "OpenClaw recognizes the Discord channel proxy in its logs but Discord REST calls and gateway connection fail with \"fetch failed\" under WSL2 when an HTTP proxy is required, despite curl succeeding through the same proxy. The issue is reproducible on OpenClaw 2026.3.13 with proxy set at ~/.openclaw/openclaw.json → channels.discord.proxy and results in gatewayConnected=false and no WebSocket establishment.", - "solution_summary": "Ensure the configured proxy is actually applied to Discord's underlying HTTP client (fetch/undici used by discord.js) and to the gateway connection, e.g., pass a proxy agent or appropriate undici/fetch proxy settings (or honor HTTP_PROXY/HTTPS_PROXY) so REST and WebSocket requests route through the proxy.", - "maintainer_signal_summary": "Reporter provided reproduction steps, config snippet, environment (WSL2), logs showing proxy-enabled messages but fetch failures, and notes high severity and 100% reproducibility; no maintainer response in the thread.", - "dedupe_summary": "Distinctive for OpenClaw 2026.3.13 on WSL2 where channels.discord.proxy is logged as enabled but REST API errors show \"fetch failed\"; likely duplicates are other reports about discord.js/undici proxy not being applied or gateway REST failing when using an HTTP proxy." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Summary captures the core issue (configured HTTP proxy is logged as enabled but Discord REST calls fail and points to discord.js/undici proxy not being applied) but includes minor noise (specific app version and environment) and extra phrasing that aren't needed for deduping." - }, - "usage": { - "input_tokens": 818, - "output_tokens": 612 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "OpenClaw's Discord REST API calls ignore configured proxy settings (channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY) and fail with \"TypeError: fetch failed\", while the Discord WebSocket and other channels (e.g., Telegram) work and curl through the proxy succeeds.", - "solution_summary": "Route Discord REST API HTTP requests through the same proxy-aware SSRF guard / transport fetch path used for web_search/web_fetch (or otherwise ensure the fetch uses the configured proxy agent) so Discord REST calls honor proxy configuration.", - "maintainer_signal_summary": "Reporter on v2026.2.26 provides config, env vars, logs, and repro steps showing WebSocket works but REST fails; changelog notes a proxy fix for web tools that explicitly did not include Discord REST, indicating an omission; curl through proxy succeeds so proxy is functional.", - "dedupe_summary": "Duplicate if issue reports 'fetch failed' or Discord REST failing to use channels.discord.proxy or HTTP_PROXY/HTTPS_PROXY, mentions gateway v2026.2.26 (or similar) and that web_search/web_fetch proxy fix excluded Discord REST." - }, - "judge": { - "score": 4, - "has_boilerplate": true, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Captures the core problem (Discord REST/fetch failing to honor channels.discord.proxy or HTTP(S)_PROXY and exclusion from a previous web_search/web_fetch proxy fix) but includes meta 'Duplicate if...' phrasing and a version number that are unnecessary noise." - }, - "usage": { - "input_tokens": 841, - "output_tokens": 710 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "The CLI command openclaw agent --channel discord --to resolves every delivery to agent:*:main instead of the live Discord channel/thread session because the session resolver does not search by real delivery context (channel + to + accountId), the command path did not propagate channel/accountId, Discord targets are not normalized, and compact resolver lookup logic is incorrect.", - "solution_summary": "Manual patches to compiled dist files: gateway-cli now resolves sessions by delivery target and normalizes Discord targets; compact uses lookup by delivery context and buildAgentMainSessionKey; register.agent passes channel and replyAccount/accountId to the resolver. Post-patch validation shows CLI routes to the correct Discord session.", - "maintainer_signal_summary": "High regression risk: fixes were applied directly to compiled files in dist/ (dist/gateway-cli-BjsM6fWb.js, dist/compact-1mmJ_KWL.js, dist/register.agent-DHoJWl4M.js) and will be lost on updates. Recommendation: implement the fix in source, add tests, and include a post-update revalidation step.", - "dedupe_summary": "Duplicate signature: CLI agent Discord delivery context resolver bug causing messages to route to agent:*:main; key artifacts changed are dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js; related issues #36401, #41562, #16149." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summarizes the core bug (CLI Discord delivery-context resolver routing to agent:*:main) and relevant compiled artifacts, but includes extraneous related-issue references that aren't needed for deduplication." - }, - "usage": { - "input_tokens": 768, - "output_tokens": 573 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions: Discord threads had volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) re-injected every turn causing context bloat and degradation; CLI agent delivery resolved to the global main session instead of the thread-bound live session because the session resolver didn't use the actual delivery context (channel+to+accountId).", - "solution_summary": "Patched compiled runtime bundles to stop reinjecting the volatile Discord metadata and to resolve sessions by actual delivery target. Changes: pi-embedded stopped injecting the listed metadata fields; gateway-cli normalizes Discord targets and resolves by delivery target; compact added delivery-context lookup for agent CLI; register.agent now passes channel and replyAccount/accountId and fixes incorrect session-key usage. Verified by restarting the gateway service and observing correct sessions and no repeated context.", - "maintainer_signal_summary": "Fixes were applied directly to compiled files in openclaw/dist (dist/pi-embedded-..., dist/gateway-cli-..., dist/compact-..., dist/register.agent-...), so the patches are manual and will be lost on update—maintainers should add these fixes to source, or maintain a re-application script post-update. Target environment: OpenClaw 2026.3.11, Discord channel, systemd deployment.", - "dedupe_summary": "Unique issue: 'discord-thread volatile metadata reinjection + CLI session resolver using main instead of thread-bound session'. Matches reports about repeated Discord-thread context bloat or CLI commands landing in agent:*:main; resolved by stopping metadata reinjection and adding delivery-context session resolution in the listed bundles." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures both core regressions (Discord-thread metadata reinjection and CLI session resolver routing to agent:*:main) and the fix approach, with no boilerplate or unrelated detail." - }, - "usage": { - "input_tokens": 687, - "output_tokens": 666 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "Sandbox media staging cap is hardcoded to 5 MB in dist/store-*.js (const MEDIA_MAX_BYTES = 5 * 1024 * 1024), so files larger than 5 MB (PDFs, PPTX, images) cannot be copied into sandboxed agents. Per-type config keys (tools.media.image.maxBytes, tools.media.audio.maxBytes) exist but there is no way to override this global staging cap. Regression observed since v2026.3.7 and present in v2026.3.8.", - "solution_summary": "Expose the staging cap as a configurable setting (e.g. tools.media.maxBytes in openclaw.json) with a sensible default (5 MB) and update the build/runtime to read and apply that value before per-type checks; add tests and documentation for the new config. Ensure the change is applied to dist files so npm installs don't require manual patching.", - "maintainer_signal_summary": "This is a user-visible regression and should be fixed in a patch release; a manual sed-based workaround exists but is not acceptable long-term. Prioritize a small change to load the config value and release an update.", - "dedupe_summary": "Likely duplicates: other issues about media size limits, sandbox staging limits, or missing tools.media.* configuration. Search for 'MEDIA_MAX_BYTES', 'sandbox media limit', or 'tools.media.maxBytes' before adding a duplicate issue." - }, - "judge": { - "score": 3, - "has_boilerplate": true, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Includes the important technical terms (MEDIA_MAX_BYTES, sandbox media limit, tools.media.maxBytes) but is phrased as triage/instruction rather than a concise 1-2 sentence summary, so it's useful but noisy." - }, - "usage": { - "input_tokens": 734, - "output_tokens": 835 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "Discord bot receives messages (Gateway/WS) but all REST API calls fail with TypeError: \"fetch failed\" despite openclaw.json showing proxy configured and logs saying \"rest proxy enabled\". Direct curl calls using the same proxy succeed, so the REST proxy path in OpenClaw appears to be broken or not used for HTTP requests.", - "solution_summary": "No fix proposed in the report. User demonstrates the proxy works externally (curl) and only OpenClaw's REST requests fail, implying a bug in OpenClaw's REST proxy handling; no workaround or code change was supplied.", - "maintainer_signal_summary": "Issue is labeled as a bug and includes logs showing \"rest proxy enabled\" and \"gateway proxy enabled\", but there are no maintainer comments or PRs referenced. The report includes environment details (OpenClaw 2026.2.25, macOS, Node 25.6.1).", - "dedupe_summary": "Unique identifiers: REST \"fetch failed\" while Gateway works; openclaw.json proxy field set and logged as enabled; curl with same proxy succeeds; OpenClaw v2026.2.25 on Node 25.6.1 — likely duplicates are issues about REST proxy not being applied or node-fetch/undici proxy integration failures." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise summary that captures the core problem (REST API requests failing with \"fetch failed\" despite proxy configured and gateway working) but includes minor noise like explicit version numbers and a speculative duplicate note that should be removed for perfect deduping." - }, - "usage": { - "input_tokens": 679, - "output_tokens": 750 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "Discord REST API calls (via @buape/carbon's RequestClient) ignore channels.discord.proxy from openclaw.json because RequestClient calls globalThis.fetch without a proxy-aware dispatcher; Node.js 22's native fetch doesn’t use HTTP_PROXY/HTTPS_PROXY unless --use-env-proxy is set, which breaks WebSocket connections.", - "solution_summary": "Make @buape/carbon's REST client accept and use an optional undici dispatcher or explicit proxy option, and have OpenClaw pass channels.discord.proxy into it; as a temporary workaround, preload a module to call setGlobalDispatcher(new EnvHttpProxyAgent()) instead of using --use-env-proxy.", - "maintainer_signal_summary": "This is a transport/config bug affecting REST calls, memorySearch embeddings, and attachment downloads in proxied environments and should be prioritized; the correct fix is wiring a proxy-aware dispatcher into the RequestClient (and related fetch paths) rather than using --use-env-proxy which breaks gateway websockets.", - "dedupe_summary": "Related to #25232 (attachments bypass proxy) — same root cause (fetch/dispatcher not proxy-aware) but different code paths; link issues and fix dispatcher usage across components rather than treating them as exact duplicates." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise identification of the root cause ('fetch/dispatcher not proxy-aware') that links related reports, but includes minor meta/instructional wording and omits the specific component name which slightly reduces precision." - }, - "usage": { - "input_tokens": 688, - "output_tokens": 876 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "Under sustained load with long-running conversations OpenClaw shows severe performance degradation: unbounded context token growth (sessions up to ~42k/90k tokens) that increases per-turn latency, Discord lane blocking causing serialization, and typing indicators timing out or remaining until TTL.", - "solution_summary": "Applied emergency config changes to limit concurrency and context size (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents.maxConcurrent 8→4, maxChildrenPerAgent 8→4, agents.defaults.contextTokens 80000) and increased Discord eventQueue.listenerTimeout 120000→300000; also improved patching script to cover more hash variants and restarted the gateway, with no new lane/typing errors observed post-restart.", - "maintainer_signal_summary": "Maintainers implemented immediate mitigations, updated the patching script, and verified the gateway is running without new errors; they note restart helps but recommend making limits adaptive or configurable for a long-term fix.", - "dedupe_summary": "Related/overlapping with #27690 and #44026 (lane blocking/serialization) and typing TTL issues (#26733/#27172); this report is distinct in providing concrete config changes and verification steps but duplicates the underlying causes (unbounded context growth and excessive concurrency)." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Captures the core causes (unbounded context growth and excessive concurrency) and links related issues, but includes minor noise about config/verification steps and explicit issue references that aren't needed for deduping." - }, - "usage": { - "input_tokens": 622, - "output_tokens": 663 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord thread handling re-injects volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, causing context contamination, token growth, prompt cache misses and performance degradation in long threads. The compiled runtime lacks a guard to limit this injection to the first turn.", - "solution_summary": "Manual edits to compiled files in dist/ prevented repeated injection and added a first-turn guard: modified dist/pi-embedded-D6PpOsxP.js to stop injecting the listed fields and updated dist/compact-1mmJ_KWL.js to add an isNewSession guard and replace resolveAgentMainSessionKey with buildAgentMainSessionKey(...). Post-patch validation shows threads no longer echo repeated context and prompt caching is restored.", - "maintainer_signal_summary": "High regression risk because fixes were applied directly to compiled dist/ files and will be lost on upgrade; maintainers should implement the fix in source, replace the patched logic in the build, and add a post-update revalidation script. Related issues: #41355, #34228, #37047.", - "dedupe_summary": "Keywords for dedupe: discord thread, thread starter re-injected every turn, metadata injection (message_id reply_to_id timestamp Chat history), prompt cache bust, dist/* compiled patch, isNewSession guard, buildAgentMainSessionKey replacement." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "The summary captures the core bug—Discord thread metadata (starter/history/message ids/timestamps) being re-injected every turn causing prompt cache busts—and includes useful technical anchors (dist files, isNewSession guard), though it's presented as keywords rather than a concise sentence and includes minor build/patch noise." - }, - "usage": { - "input_tokens": 634, - "output_tokens": 611 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "In v2026.3.11 Discord thread messages repeatedly inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, degraded performance, token bloat and prompt-cache busting.", - "solution_summary": "Applied a manual patch to dist/pi-embedded-D6PpOsxP.js to prevent unconditional metadata injection for Discord threads — metadata is now injected only on the first turn; validation shows repeated context removed and prompt caching restored.", - "maintainer_signal_summary": "HIGH regression risk: change was made to a compiled file in dist and will be lost on update/reinstall; maintainers should apply an official source-level fix and keep a post-update revalidation script.", - "dedupe_summary": "Related/duplicate reports: #41355, #34228, #37047 — all describe Discord thread starter/history re-injection or thread session resets and should be linked/considered duplicates." - }, - "judge": { - "score": 3, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "The summary contains the key technical phrase (Discord thread starter/history re-injection) useful for deduping but is too terse and does not succinctly state the core bug (metadata re-injection every turn) or root cause." - }, - "usage": { - "input_tokens": 642, - "output_tokens": 618 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "Two runtime regressions in Discord threads: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing unbounded context growth and a session resolver that falls back to a global agent (agent:*:main) instead of selecting live thread-bound sessions, leading to wrong CLI/session routing and increasing latency over long threads.", - "solution_summary": "Applied manual runtime patches to compiled bundles to stop repeated metadata injection and to make the resolver lookup by delivery context (channel + to + accountId); modified dist/reply-BCcP6j4h.js, dist/compact-1mmJ_KWL.js, dist/pi-embedded-Cz5VjpnY.js, dist/gateway-cli-B-E8XzUM.js, dist/register.agent-jS8B5meo.js, added an idempotent re-application script and backups, and restarted the gateway.", - "maintainer_signal_summary": "Hotpatches were applied to production bundles and the gateway was restarted and validated (no new errors, thread-bound sessions resolving, CLI routing correct); maintainers note these are manual edits that will be lost on updates and provided an idempotent reapply script plus backups for operational continuity.", - "dedupe_summary": "Keywords: discord threads, context growth, repeated metadata injection, session resolver fallback to agent:*:main, CLI routing wrong, patched dist/*.js, hotpatch with reapply script, OpenClaw 2026.3.11." - }, - "judge": { - "score": 4, - "has_boilerplate": true, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Captures the core bug (Discord thread context growth from repeated metadata and session resolver fallback causing CLI routing to wrong session) and modified artifacts, but includes an unnecessary version number and a minor operational detail (hotpatch/reapply) that aren't helpful for deduplication." - }, - "usage": { - "input_tokens": 618, - "output_tokens": 594 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "After updating, OpenClaw receives Discord messages and generates responses but fails to send replies with \"TypeError: fetch failed\" despite logs showing the Discord REST and gateway proxies enabled and curl succeeding through the same proxy. Environment: OpenClaw 2026.3.2 on Node.js 22.22.0 (Linux systemd); proxy is http://127.0.0.1:7890 (Clash).", - "solution_summary": "Ensure the HTTP(S) proxy is actually applied to the fetch calls used to post replies (including any multipart/attachment endpoints) — either by passing a proxy/agent to the fetch implementation used by the Discord client, setting HTTP_PROXY/HTTPS_PROXY for the systemd service, or switching to a fetch/undici configuration that supports proxying. Add logging/tests for the specific message-send code path to confirm the agent is used. ", - "maintainer_signal_summary": "Investigate the reply-send code path to find where fetch is invoked without a proxy agent or environment variables; add a fix to propagate the configured proxy to all REST requests (including file uploads), add unit/integration tests, and confirm behavior under systemd environment variables. ", - "dedupe_summary": "Likely duplicate of issues where native fetch/undici in Node ignores proxy settings or specific Discord message endpoints bypass the configured REST proxy; also related to problems where systemd service lacks HTTP_PROXY/HTTPS_PROXY env vars or where multipart uploads don't use the proxy agent." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summarizes specific likely root causes (native fetch/undici ignoring proxy, Discord REST bypass, missing HTTP_PROXY/HTTPS_PROXY in systemd, multipart uploads not using proxy) that are relevant for clustering, though it lists multiple hypotheses rather than a single concise cause." - }, - "usage": { - "input_tokens": 558, - "output_tokens": 818 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "In v2026.3.11 the CLI command `openclaw agent --channel discord --to ...` resolves to the fallback session `agent:*:main` instead of the live Discord channel/thread session because the compiled session resolver does not lookup sessions by delivery context and fails to normalize Discord targets.", - "solution_summary": "A manual patch was applied to dist/gateway-cli-BjsM6fWb.js to normalize Discord targets (e.g. `148...` → `channel:148...`) and add a session lookup by delivery context (channel+to+accountId) before falling back to main, restoring correct routing to the live session.", - "maintainer_signal_summary": "Patch was made directly to compiled dist files (high regression risk and will be lost on update); this needs a proper fix in source code and build artifacts plus tests to persist. Related issues: #36401 and #41562; user requests an official fix to avoid repeated hotfixes.", - "dedupe_summary": "Distinctive fingerprint: v2026.3.11 compiled CLI (dist/gateway-cli-*.js) lacking delivery-context session lookup and Discord target normalization, causing fallback to agent:*:main; may overlap with other session-routing/Discord-thread issues (e.g. #41562) but is specific to the compiled-dist hotfix scenario." - }, - "judge": { - "score": 4, - "has_boilerplate": true, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary clearly captures the core bug (compiled CLI dist/gateway-cli-*.js missing delivery-context lookup and Discord target normalization causing fallback to agent:*:main) but includes minor noisy elements like a version/issue reference and “Distinctive fingerprint” phrasing that aren’t needed for deduping." - }, - "usage": { - "input_tokens": 623, - "output_tokens": 848 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "In v2026.3.11 the compiled file dist/compact-1mmJ_KWL.js was patched to add delivery context lookup but incorrectly calls a non-existent function resolveAgentMainSessionKey, causing ReferenceError at runtime and breaking the agent command (possible gateway crashes).", - "solution_summary": "Locally patched dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with the correct buildAgentMainSessionKey(...); this removes the ReferenceError and restores agent/session behavior. The fix is applied to the compiled output and validated.", - "maintainer_signal_summary": "High priority: this is a regression introduced by the previous session-resolver fix (#3) and will recur if the original source/patch is reapplied; the correct change must be made in source and recompiled rather than only editing dist/ to avoid losing the fix on upgrades.", - "dedupe_summary": "Regression: ReferenceError: resolveAgentMainSessionKey is not defined in dist/compact-*.js due to incorrect function name used by the session-resolver patch; correct function is buildAgentMainSessionKey." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that names the regression, error, affected compiled file pattern, incorrect function used, and the correct function to match duplicate reports." - }, - "usage": { - "input_tokens": 594, - "output_tokens": 533 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "The discord channel's proxy setting (channels.discord.proxy) is only applied to WebSocket connections, not the REST HTTP client. As a result health checks and message sends behind a proxy time out with \"fetch failed\" while direct curl via the same proxy succeeds.", - "solution_summary": "Make the Discord plugin use the configured channels.discord.proxy for its REST HTTP client and health check (not just WebSocket), so all Discord HTTP requests route through the proxy; add/update tests and ensure the gateway/CLI uses the same proxied client.", - "maintainer_signal_summary": "Regression: previously worked, now fails in OpenClaw 2026.3.8 (commit 3caab92). Repro steps provided (set channels.discord.proxy, curl works, openclaw message send fails). Affects users behind proxies and blocks send/health functionality.", - "dedupe_summary": "Keywords: \"fetch failed\", channels.discord.proxy, Discord REST API proxy, WebSocket-only proxy, openclaw message send timeout, health check fetch failed, OpenClaw 2026.3.8, gateway → discord channel." - }, - "judge": { - "score": 3, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "The summary contains the core signals (channels.discord.proxy not applied to Discord REST API, health check 'fetch failed', WebSocket-only proxy) but is presented as a noisy keyword list, includes an unnecessary version number, and lacks a concise 1–3 sentence description." - }, - "usage": { - "input_tokens": 550, - "output_tokens": 689 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "En v2026.3.11, el session resolver dentro de dist/compact-1mmJ_KWL.js no realiza lookup por delivery context (channel+to+accountId), por lo que las sesiones se resuelven incorrectamente a agent:*:main en lugar de la sesión viva del canal objetivo.", - "solution_summary": "Parche manual en dist/compact-1mmJ_KWL.js: se añadió lookup por delivery context (channel+to+accountId) en el resolver del comando agent y el fallback a main solo se usa si no existe una sesión específica.", - "maintainer_signal_summary": "Alto riesgo de regresión: el cambio fue aplicado en archivos compilados en dist y se perderá con futuras actualizaciones; la corrección debe incorporarse al código fuente y recrearse en el build/release.", - "dedupe_summary": "Coincidir duplicados por versión (v2026.3.11), ruta de archivo (dist/compact-1mmJ_KWL.js) y síntoma (resolver no busca delivery-context y cae a agent:*:main) para distinguirlo de otros problemas de sesión." - }, - "judge": { - "score": 3, - "has_boilerplate": true, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Summary captures the key symptom and file path (session resolver in dist/compact-1mmJ_KWL.js lacking delivery-context lookup) but includes version metadata and meta-instruction phrasing instead of a concise problem statement, adding noise." - }, - "usage": { - "input_tokens": 564, - "output_tokens": 770 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "The Control UI has most interface text hardcoded in English (settings, buttons, agent/skill pages, channel configs, status messages), creating usability barriers for non-English users; compiled assets contain literal strings like \"Settings\", \"Add\", \"Delete\", etc.", - "solution_summary": "Extract UI strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, provide a language selector or auto-detect system language, and build infrastructure to add more languages later.", - "maintainer_signal_summary": "No maintainer comments in the report; the author requests roadmap status and contributors, noting docs already include zh-CN which signals project interest in Chinese support.", - "dedupe_summary": "Likely a standalone feature request for UI localization but may overlap with any existing i18n, localization, or UI internationalization/refactor issues—check for prior i18n/translation or UI refactor tickets before acceptance." - }, - "judge": { - "score": 2, - "has_boilerplate": true, - "signal_density": 2, - "would_cluster_correctly": false, - "reasoning": "The summary is vague and meta—it notes a UI localization feature but omits key specifics (OpenClaw Control UI, zh‑CN, extracting strings) and includes procedural/acceptance instructions, so it contains boilerplate and would likely cause noisy or incorrect clustering." - }, - "usage": { - "input_tokens": 485, - "output_tokens": 493 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "WhatsApp group chats could not use the channel-level systemPrompt at account or group granularity, preventing per-account or per-group prompt customization and wildcard defaults for groups without their own prompt.", - "solution_summary": "Added systemPrompt support to WhatsApp config/schema at channel, account, and group levels; implemented resolution hierarchy where account-level prompt overrides root, group prompt resolves to specific group then wildcard, account.groups fully replace root.groups, and the final injected prompt is account prompt + resolved group prompt. Included schema/types changes, account resolution merge, group prompt composition, tests and docs.", - "maintainer_signal_summary": "PR includes tests and docs, closes the linked issue, is backward-compatible with no security/network changes, the author addressed bot review conversations, and this is a focused replacement for a previous larger PR; no changes to non-WhatsApp channels or hot-reload behavior.", - "dedupe_summary": "WhatsApp: add account/group systemPrompt hierarchy (account overrides root, group specific→wildcard fallback, account.groups replace root.groups) and compose final prompt as account + group." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused on the WhatsApp systemPrompt hierarchy and final composition, with specific behaviors (account overrides, wildcard fallback, account.groups replacing root.groups) that will help cluster similar changes." - }, - "usage": { - "input_tokens": 2261, - "output_tokens": 549 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages received via messages.upsert(type=\"append\") were being dropped before agent processing, and WhatsApp group policy helpers resolved without accountId so account-scoped settings (channels.whatsapp.accounts. / groupAllowFrom / allowFrom) could be ignored, causing allowlist fallback to fail closed.", - "solution_summary": "Enable append-path processing only for WhatsApp group messages (leave DM append skipped) and propagate accountId / resolve the WhatsApp account when evaluating group gating so account-scoped groupAllowFrom/allowFrom are considered for allowlist fallback.", - "maintainer_signal_summary": "Bug fix touching WhatsApp inbound and group activation code (e.g. src/web/inbound/monitor.ts, src/web/auto-reply/monitor/group-activation.ts). No config schema or dist changes; tests added and local traces validated that groups now enqueue to the agent and account-scoped gating behaves correctly. Risk: historical group backlog may be processed; rollback by reverting the PR or restoring the two touched files.", - "dedupe_summary": "Two distinct issues fixed: (1) group append events were skipped and never enqueued to the agent; (2) group policy resolution omitted accountId so account-scoped allowlist/require-mention settings could be ignored or fail closed." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the two technical bugs (WhatsApp group append events not enqueued; policy resolution missing accountId affecting account-scoped allow/require-mention) with no template noise, suitable for deduplication." - }, - "usage": { - "input_tokens": 1813, - "output_tokens": 558 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Canvas (A2UI) returned 401 when accessed via Tailscale Serve because resolveClientIp() returned undefined for requests from a trusted proxy that omitted X-Forwarded-* headers, and isLocalDirectRequest() did not fall back to the socket remoteAddress when allowRealIpFallback was enabled and XFF resolved to a non-loopback Tailscale IP.", - "solution_summary": "Fixes: resolveClientIp() now returns the socket address when no forwarding headers are present; isLocalDirectRequest() checks req.socket.remoteAddress when allowRealIpFallback is enabled; isLocalishHost() now requires an allowTailscale flag to accept .ts.net hosts; auth.allowTailscale is threaded through callers and docs updated with required gateway.trustedProxies and allowRealIpFallback guidance. No new config keys introduced.", - "maintainer_signal_summary": "Depends on PR #50636 and closes #28472; tests updated and passing; .ts.net acceptance is now gated by allowTailscale (which still defaults to true when tailscale.mode = \"serve\"), so reviewers should verify trustedProxies/allowRealIpFallback behavior and watch for cases where direct-proxy socket fallback changes other callers.", - "dedupe_summary": "Unique to Tailscale Serve 401: fallback resolveClientIp->socket address and allowRealIpFallback socket check, plus gating .ts.net host acceptance behind allowTailscale and threading that flag through auth callsites (depends on #50636)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the Tailscale Serve 401 root causes and precise code changes (.socket fallback, allowRealIpFallback check, .ts.net gating and flag threading) with no template noise, so it will cluster with related fixes." - }, - "usage": { - "input_tokens": 1652, - "output_tokens": 712 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "checkInboundAccessControl computes isSelfChat but never enforces it; when WhatsApp channel is configured without an explicit groupPolicy, resolveWhatsAppRuntimeGroupPolicy defaults to \"open\" so non-self messages (groups, strangers, broadcasts) bypass the self-chat isolation boundary and reach downstream processing.", - "solution_summary": "Add an early-return guard in checkInboundAccessControl that, when account.selfChatMode === true, blocks all non-self messages before any group/DM policy evaluation. The guard checks only the explicit selfChatMode flag (not the heuristic isSelfChat) and preserves self-messages via isSamePhone; isSelfChat is still returned for UI/logging.", - "maintainer_signal_summary": "All relevant unit tests passed (9 tests), code-path analysis and manual verification confirm guard placement before policy resolution, scope is limited to src/web/inbound/access-control.ts, and the change is intentionally narrow (explicit selfChatMode only); rollback is trivial by removing the early-return block.", - "dedupe_summary": "Distinct fix: enforces explicit selfChatMode hard-block in access-control.ts for WhatsApp to stop non-self messages; look for PRs touching groupPolicy defaults or auto-detection heuristics—this change is not a general groupPolicy/defaults change nor an auto-detect tweak and should not be merged as a duplicate of those. " - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary succinctly captures the core code change (hard-block selfChatMode in access-control.ts to stop non-self WhatsApp messages) but includes an unnecessary meta/instructional clause about merging/PR lookup that adds minor noise." - }, - "usage": { - "input_tokens": 1367, - "output_tokens": 608 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "With groupPolicy set to \"allowlist\", group messages from senders not in groupAllowFrom were completely dropped at the access-control layer and never reached the mention-gating/context-storage logic, so the agent could not see those messages as context.", - "solution_summary": "Introduces an opt-in config flag groupContextFromAll (top-level and per-account) that, when true, causes access-control to return storeForContext for non-allowlisted senders; those messages are passed through as contextOnly, stored in pending group history, do not trigger replies, and skip read receipts.", - "maintainer_signal_summary": "Change is opt-in and backward compatible (defaults false), per-account config takes precedence, and changes are localized to access-control → monitor → auto-reply flow; reviewers should watch allowlist normalization/compare logic and the fact that early context-only handling bypasses some existing group gating/state updates.", - "dedupe_summary": "Unique aspects: new groupContextFromAll flag, new access-control outcome storeForContext, contextOnly added to WebInboundMessage, threaded handling in monitor/on-message to record pending history without triggering replies; opt-in preserves existing behavior so duplicates should consider these structural changes rather than behavior regressions." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise and focused on the key code changes (new groupContextFromAll flag, storeForContext outcome, contextOnly field, pending-history handling) that implement storing non-allowlisted group messages as context; contains a small meta remark about duplicates but otherwise provides strong clustering signal." - }, - "usage": { - "input_tokens": 1384, - "output_tokens": 500 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "Add an optional sessionKey parameter to sessions_spawn so spawned sub-agents can reuse a deterministic session (preserve conversation history) instead of creating a new random UUID session each time. Short keys are namespaced to agent:{agentId}:subagent:{key}, fully-qualified keys are validated to prevent cross-agent session injection, and omission falls back to crypto.randomUUID().", - "solution_summary": "Implemented sessionKey handling, namespacing for short keys, validation of fully-qualified keys (reject if embedded agentId != targetAgentId), and fallback to random UUID; added tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch rejection. Also addresses a security finding about session key allowlist bypass. ", - "maintainer_signal_summary": "Reviewer flagged three blocking issues: a test file imports a non-existent module causing compile failure, a case-sensitivity bug in fully-qualified key validation (embedded agentId not normalized), and inconsistent error handling (throw vs jsonResult) for mismatch checks; labels indicate this touches agents and is size M. ", - "dedupe_summary": "Keywords: sessions_spawn sessionKey, deterministic sub-agent sessions, agent:{agentId}:subagent:{key}, fully-qualified key validation, cross-agent injection prevention, crypto.randomUUID fallback, test import error, case-sensitivity bug, inconsistent error handling." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Summary captures the core change (sessionKey for deterministic sub-agent sessions, mapping format, validation, and UUID fallback) but includes extra noisy items (test import error, case-sensitivity bug, inconsistent error handling) that weren't central and slightly dilute the signal." - }, - "usage": { - "input_tokens": 1437, - "output_tokens": 899 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) was using editMessageText, causing Telegram to return 400 \"there is no text in the message to edit\" and leaving edits failing/silent for captionable messages.", - "solution_summary": "editMessageTelegram now detects that specific 400 (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption for captionable messages; buttons-only updates use editMessageReplyMarkup directly; MESSAGE_NOT_MODIFIED is treated as success and HTML parse fallback is preserved.", - "maintainer_signal_summary": "Change is limited to the Telegram adapter (src/telegram/send.ts), backward compatible with no API/interface changes, and covered by unit tests for retry, buttons-only, error propagation, and MESSAGE_NOT_MODIFIED handling; risk is the regex failing if Telegram changes the error wording, but failures fall back to previous behavior.", - "dedupe_summary": "Unique fix: auto-detect captionable messages by catching the specific Telegram 400 and retrying with editMessageCaption, plus a direct reply-markup path; preserves public message tool interface and only touches the Telegram send implementation." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core bugfix (catch Telegram 400, retry with editMessageCaption, handle reply-markup separately) and notes scope-limitation without extraneous template or testing details." - }, - "usage": { - "input_tokens": 1318, - "output_tokens": 519 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "A prior change (#24962) hardcoded disableBlockStreaming: true for WhatsApp to stop reasoning-content leaks, which had the unintended effect of disabling block-streamed/progressive delivery entirely even when agents or per-account config enabled it.", - "solution_summary": "The deliver callback now only suppresses kind === \"tool\" payloads and disableBlockStreaming is read from channels.whatsapp.blockStreaming (falling back to agents.defaults.blockStreamingDefault), restoring config-driven block streaming while reasoning suppression remains handled upstream.", - "maintainer_signal_summary": "Author ran contract tests and live WhatsApp verification (progressive delivery confirmed), fixed failing tests, resolved bot review items, and left only reviewer/maintainer judgment items outstanding; change is backward compatible and releasable.", - "dedupe_summary": "Key identifiers: bug introduced by #24962 (hardcoded disableBlockStreaming), fix restores per-account/global blockStreaming config, reasoning/tool payload suppression behavior unchanged; keep if issue references those PR/issue numbers or the WhatsApp process-message change." - }, - "judge": { - "score": 4, - "has_boilerplate": true, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Captures the core bug and fix (hardcoded disableBlockStreaming from #24962 vs restoring per-account/global config and payload suppression behavior) but includes unnecessary meta/instructional text and PR references that are borderline boilerplate." - }, - "usage": { - "input_tokens": 1310, - "output_tokens": 566 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Telegram gateway hung when users sent videos larger than the configured mediaMaxMb (default 5MB). getFile returned OK but fetchRemoteMedia threw a content-length enforcement error leaving the response body unconsumed, which caused undici Agent.close() in closeDispatcher() to deadlock and make the gateway unresponsive.", - "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5MB to 20MB, added timeouts/retries to getFile and media downloads, made media download failures non-fatal so captions/text are still forwarded, and fixed closeDispatcher() to avoid Agent.close() deadlocks by timing out and calling destroy() as a fallback; also extended stripInboundMetadata to hide inbound media paths in the UI.", - "maintainer_signal_summary": "This is a tested bug fix and security hardening for the Telegram channel that prevents production outages; change is backward-compatible and configurable (mediaMaxMb), and should be merged promptly to restore reliability for bots receiving videos.", - "dedupe_summary": "Telegram integration hang due to unconsumed response body causing undici Agent.close deadlock when media exceeds maxBytes; fix: increase mediaMaxMb to 20, add download timeouts/retries, make media errors non-fatal, and add timeout+destroy fallback in closeDispatcher()." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that identifies the exact root cause (undici Agent.close deadlock from unconsumed response bodies when media exceeds maxBytes) and lists the targeted fixes, so it will cluster correctly." - }, - "usage": { - "input_tokens": 1406, - "output_tokens": 597 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "When Telegram users send document (PDF/DOCX) attachments with no caption, the code falls back to a hardcoded \"\" placeholder in resolveTelegramInboundBody, causing the agent to misidentify documents as images and mishandle them.", - "solution_summary": "Replace the hardcoded \"\" fallback with the existing type-aware placeholder variable so inbound body text correctly becomes \"\", \"\", etc.; add unit tests covering all media types and verify getFile is triggered for documents.", - "maintainer_signal_summary": "This is a small, backward-compatible bug fix scoped to agent-facing body text (closes #7116); media download and extraction logic are unchanged, unit tests were added (extensions/telegram/src/bot/helpers.test.ts and bot.create-telegram-bot.test.ts), and tests pass.", - "dedupe_summary": "Fix targets resolveTelegramInboundBody: replace hardcoded '' with the computed placeholder; key test files added to lock behavior for document/media placeholders and getFile invocation; change is limited to placeholder text and should be deduplicable by these file/method names and the ''→placeholder replacement." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summarizes the core change—replacing hardcoded '' in resolveTelegramInboundBody with a type-aware placeholder—though it includes minor noise about added tests and deduplication expectations that aren't strictly necessary." - }, - "usage": { - "input_tokens": 1233, - "output_tokens": 490 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "MEDIA_MAX_BYTES was hardcoded to 5 MB in src/media/store.ts, so files larger than 5 MB silently failed to stage into sandboxed containers and could not be processed by agents.", - "solution_summary": "Introduced a new optional config field media.maxBytes (bytes, max 500 MB) and replaced the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(); the gateway reads media.maxBytes on boot, error messages now report the configured limit, and the 5 MB default is preserved when unset.", - "maintainer_signal_summary": "All relevant unit tests pass; bot review threads were resolved (ensure set/reset behavior and MB formatting); change is backward compatible and reversible by removing the config; note risk of disk fill if users set the limit very high (schema enforces 500 MB cap).", - "dedupe_summary": "Make the media staging/store size limit configurable via media.maxBytes instead of a hardcoded 5 MB, with gateway applying the value on boot and a schema-enforced 500 MB maximum; default remains 5 MB." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states the core change (make 5 MB media staging limit configurable via media.maxBytes), how it's applied (gateway boot), that default remains, and notes the schema-enforced 500 MB cap, with no boilerplate or noise." - }, - "usage": { - "input_tokens": 1354, - "output_tokens": 647 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "When Telegram media download fails (due to exceeding mediaMaxMb or Telegram API errors) the handler returned early and the entire message—including caption text—was silently dropped, so the agent received no message or ack and the user got no feedback.", - "solution_summary": "On download failure the code now extracts file metadata (name, size, type) from the Telegram message object, injects a metadata stub into msg.text (e.g. \"⚠️ [File: name, 40MB] (exceeds 5MB)\"), and lets the message continue through the normal pipeline instead of returning early.", - "maintainer_signal_summary": "This is a bug fix in the Telegram integration (src/telegram/bot-handlers.ts), backward-compatible and limited to the error path; it removes the prior automatic reply-to-user messages and notes a small risk from mutating msg.text but cites easy revert by reverting the commit.", - "dedupe_summary": "Specifically fixes silent drops on media download errors and size-limit cases (mediaMaxMb and Telegram getFile limit); does not change normal download paths, media group handling, sticker processing, or outbound file sending, so it's distinct from general media handling refactors." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states the core fix (preventing silent drops on Telegram media download or size-limit failures) using relevant technical terms and clarifies scope, so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 1196, - "output_tokens": 484 - } - }, - { - "thread_id": 5796, - "number": 24117, - "kind": "pull_request", - "title": "feat(cli): add --session-key targeting for openclaw agent", - "summary": { - "problem_summary": "The CLI lacks a --session-key option, so CLI-triggered deliveries in multi-channel/multi-agent setups can resolve to an agent main session instead of a specific channel because sessionId is UUID-based and not externally discoverable and --to may be unavailable in callback contexts.", - "solution_summary": "Adds a first-class --session-key flag to openclaw agent (register.agent.ts), extends AgentCliOpts with sessionKey, forwards it into resolveSessionKeyForRequest in src/commands/agent-via-gateway.ts, preserves resolution precedence (session-key → session-id → --to → agent) and updates help/validation and tests.", - "maintainer_signal_summary": "Change is additive and backward-compatible, tests for CLI parsing and gateway forwarding were added and passing, and the change is low-risk and scoped to CLI option parsing and session resolution pass-through.", - "dedupe_summary": "Key identifiers: --session-key flag, resolveSessionKeyForRequest, AgentCliOpts, agent-via-gateway, register.agent.ts; duplicates should match PRs adding a sessionKey CLI option or forwarding sessionKey through gateway with same precedence semantics." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: names the new --session-key flag, related functions/files, and the exact duplicate-match criteria, so it captures the core change without noise." - }, - "usage": { - "input_tokens": 1168, - "output_tokens": 479 - } - }, - { - "thread_id": 766, - "number": 11611, - "kind": "pull_request", - "title": "feat: separate group-level allowlist from sender-level command authorization", - "summary": { - "problem_summary": "With groupPolicy: \"allowlist\" the code currently gates all group interactions by sender phone number (groupAllowFrom), preventing: (1) allowlisting groups by JID while letting any participant chat, (2) restricting slash commands to owner only, and (3) blocking spam groups while keeping approved groups open.", - "solution_summary": "When groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access is gated by the groups JID allowlist (via resolveChannelGroupPolicy) so only listed groups are accepted and anyone in an accepted group can chat; groupAllowFrom continues to control slash-command authorization. If no groups mapping is present, existing sender-based filtering is preserved. Code changes touch src/web/inbound/access-control.ts, JSDoc for WhatsApp config, and add unit tests; depends on #3326.", - "maintainer_signal_summary": "Change appears localized and safe (confidence ~4/5). Tests were added to cover the new branches and docs updated; one outdated inline comment was flagged as a minor doc/cleanup item. Reviewers should note the dependency on #3326 and the intended behavior change when a groups mapping is present.", - "dedupe_summary": "Unique change: introduce a group-JID allowlist that supersedes sender-based gating when channels.whatsapp.groups exists, while keeping groupAllowFrom dedicated to command authorization; preserves legacy behavior when groups is absent. Files: access-control.ts, config types, and new tests; depends on PR #3326." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Clearly states the core change—adding a group-JID allowlist that supersedes sender-based gating while retaining groupAllowFrom for command authorization and preserving legacy behavior—though it includes minor noise about files/PR dependency." - }, - "usage": { - "input_tokens": 1218, - "output_tokens": 614 - } - }, - { - "thread_id": 17924, - "number": 46425, - "kind": "pull_request", - "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", - "summary": { - "problem_summary": "Configured mediaMaxMb is ignored for Telegram outbound reply messages because loadWebMedia in the reply delivery path never receives a maxBytes parameter and falls back to the 100 MB default.", - "solution_summary": "Thread mediaMaxBytes through the Telegram outbound delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia receives the configured limit; added two unit tests to verify behavior.", - "maintainer_signal_summary": "All related tests pass (108 total across delivery, dispatch, native-commands, bot-message); change is backward-compatible when mediaMaxBytes is unset, does not alter direct-send behavior or media internals, and closes #46023.", - "dedupe_summary": "Propagate mediaMaxBytes to reply delivery so Telegram outbound replies obey mediaMaxMb; small, low-risk parameter-threading bugfix with tests and no behavioral change when unset." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that names the exact parameter and affected path (Telegram outbound reply delivery) and notes it's a low-risk bugfix, so it will cluster well with similar fixes." - }, - "usage": { - "input_tokens": 1219, - "output_tokens": 570 - } - }, - { - "thread_id": 5712, - "number": 37050, - "kind": "pull_request", - "title": "fix(telegram): add HTML parse fallback for media captions", - "summary": { - "problem_summary": "Telegram media sends (photo, document, video, animation, audio, voice, video note) failed with a 400 \"can't parse entities\" error when captions contained invalid HTML entities because only text messages used an HTML-parse fallback; the media path had no retry and the entire send would fail.", - "solution_summary": "Wrapped the media send callback in withTelegramHtmlParseFallback so on an HTML parse error the code retries the send without parse_mode and with the raw caption (plain text). This change applies to all media types and includes a unit test that mocks sendPhoto rejecting then succeeding to verify the retry behavior.", - "maintainer_signal_summary": "Bug fix only, backward-compatible, unit test added; change limited to src/telegram/send.ts (media send inner callback) and does not affect text/edit/thread/caption-splitting logic. Can be reverted by restoring the original sendMedia implementation if needed.", - "dedupe_summary": "Add HTML-parse fallback for Telegram media captions: retry media send without parse_mode on \"can't parse entities\" 400; previously only text messages retried; unit test added; change in src/telegram/send.ts." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core change—adding a retry fallback for Telegram media captions on HTML-parse errors—without extraneous template text, so it would cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 1093, - "output_tokens": 486 - } - }, - { - "thread_id": 21769, - "number": 55280, - "kind": "pull_request", - "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", - "summary": { - "problem_summary": "WhatsApp had block streaming effectively disabled (disableBlockStreaming hardcoded true) and a race in the block-reply pipeline: block replies were enqueued optimistically and the pipeline marked content as sent before actual delivery, causing “silent turns” where neither intermediate blocks nor final reply reached WhatsApp.", - "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming, add sensible coalescing defaults, fix activation logic (remove undefined fallthrough), introduce sendBlockReplyAsync that resolves/rejects on real delivery, and await it in onBlockReply so sentContentKeys is set only after confirmed delivery. Tests and mocks were updated accordingly.", - "maintainer_signal_summary": "This PR fixes the root cause of the silent-turn bug reverted in #24962, restores configurable block streaming consistent with other channels, and includes unit and manual verification; reviewers should focus on process-message.ts, reply-dispatcher.ts, dispatch-from-config.ts, and the activation logic change.", - "dedupe_summary": "Keywords/unique points: WhatsApp blockStreaming hardcoded true, disableBlockStreaming activation bug (undefined vs false), delivery race marking sentContentKeys too early, sendBlockReplyAsync added, block-streaming coalesce defaults, modified files process-message.ts/shared.ts/reply-dispatcher.ts/dispatch-from-config.ts; fixes silent turns and enables per-account/global channels.whatsapp.blockStreaming." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that highlights the root cause (hardcoded disableBlockStreaming and delivery race), the configurable change (per-account/global channels.whatsapp.blockStreaming), and key code fixes (sendBlockReplyAsync, sentContentKeys timing) without template noise." - }, - "usage": { - "input_tokens": 1006, - "output_tokens": 656 - } - }, - { - "thread_id": 8098, - "number": 25543, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked per-group allowlist support—only sender-level groupAllowFrom existed—so specific Signal groups couldn't be explicitly allowed to bypass sender checks or run control commands.", - "solution_summary": "Introduces channels.signal.groups. with a new SignalGroupConfig and Zod schema, wires resolveChannelGroupPolicy into the Signal event handler and command authorizers so listed groups bypass sender-level checks, and updates docs.", - "maintainer_signal_summary": "Reviewer (Greptile) endorsed changes as safe to merge (confidence 5/5); TypeScript compilation passes, manually tested, addresses prior PR feedback (commandGate integration) and maintains backwards compatibility via hasGroupAllowFrom.", - "dedupe_summary": "Key identifiers: config path channels.signal.groups., types SignalGroupConfig/SignalAccountConfig, schema SignalGroupSchema/SignalAccountSchemaBase, function resolveChannelGroupPolicy, commandGate authorizers, modified files src/config/types.signal.ts, src/config/zod-schema.providers-core.ts, src/signal/monitor/event-handler.ts, docs/channels/signal.md; fixes #25540, supersedes #4337." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that names the added config (channels.signal.groups.), related types/schemas, handler function, and touched files—providing the exact technical signals needed to cluster similar changes." - }, - "usage": { - "input_tokens": 948, - "output_tokens": 681 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal lacked group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs the access check treated those as sender IDs (phone/UUID) and always failed, causing group messages to be silently dropped.", - "solution_summary": "Added a groups config to SignalAccountSchemaBase (same shape as other channels) and invoked resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom; added four tests to cover Signal group-level policy resolution.", - "maintainer_signal_summary": "Small, low-risk change that mirrors other channels: schema addition + access-check update; includes tests and claims to fix #25540 with all relevant tests passing.", - "dedupe_summary": "Detect duplicates by the combination of keywords: \"SignalAccountSchemaBase groups\", \"resolveChannelGroupPolicy\", \"group-level allowlisting\", \"groupAllowFrom mismatch\", and issue #25540." - }, - "judge": { - "score": 2, - "has_boilerplate": true, - "signal_density": 2, - "would_cluster_correctly": true, - "reasoning": "The entry includes useful technical keywords but is framed as a detection instruction and omits the core change (Signal lacked group-level allowlisting and the fix adds a groups config to SignalAccountSchemaBase), so it's noisy and incomplete for deduplication." - }, - "usage": { - "input_tokens": 748, - "output_tokens": 638 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "WhatsApp delivery currently batches all intermediate assistant text into a single message because process-message.ts hardcodes disableBlockStreaming: true, so tool-call/sleep separated blocks are not sent as separate messages.", - "solution_summary": "Respect channels.whatsapp.blockStreaming by adding resolveWhatsAppBlockStreamingEnabled() and replacing the hardcoded flag, register blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in dock, and treat intermediate block payloads as delivered when streaming is enabled; tests updated accordingly.", - "maintainer_signal_summary": "Labels: channel: whatsapp-web, app: web-ui, agents, size: S. Tests updated (process-message.inbound-contract.test.ts) to match other channels; all 10 tests pass and anyReplyDelivered verifies intermediate block delivery. Change is backwards-compatible when blockStreaming is false or omitted.", - "dedupe_summary": "Enable config-driven WhatsApp block streaming (channels.whatsapp.blockStreaming) so intermediate blocks can be delivered separately instead of being hardcoded to batch; includes code changes, dock registration, and updated tests." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change—making WhatsApp block streaming configurable so intermediate blocks are delivered—while only slightly including minor implementation/test notes that don't harm deduplication." - }, - "usage": { - "input_tokens": 498, - "output_tokens": 493 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v2-signal-focused.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v2-signal-focused.json deleted file mode 100644 index 38a7542..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v2-signal-focused.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v2-signal-focused", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "You are extracting the core signal from a GitHub issue or pull request for duplicate detection. The output will be embedded and clustered with similar issues.\n\nYour job is to answer: \"What is this issue/PR fundamentally about?\" Ignore everything else.\n\nStrip away ALL of the following — they add noise to embeddings:\n- Template boilerplate (headings like \"### Steps to reproduce\", \"### Expected behavior\")\n- Testing notes, QA instructions, review checklists\n- Environment details (OS, browser, versions) unless they ARE the core issue\n- Deployment procedures, rollback plans\n- Links, references to other issues\n- Formatting artifacts (checkboxes, horizontal rules, badges)\n\nWhat to capture:\n- The specific bug, feature, or change in concrete terms\n- Which component/module/API/subsystem is affected\n- Why it matters (the user impact, not the testing impact)\n\nReturn JSON with keys problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nRules: plain text only, no markdown, 1-3 sentences each.\ndedupe_summary is the most important field — it must be a clean, specific description that would match other reports about the same underlying issue.", - "timestamp": "2026-03-30T16:29:16.007Z", - "aggregate": { - "avg_score": 4.97, - "avg_signal_density": 4.97, - "boilerplate_count": 0, - "boilerplate_pct": 0, - "would_cluster_correctly_pct": 100, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 47983, - "total_output_tokens": 25794 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "channels.discord.proxy only applies to the Discord WebSocket Gateway; REST API calls (command deploy, fetch bot identity) and guild resolution do not use the configured HTTP proxy, causing startup REST timeouts and unresolved guild channels so the bot is unresponsive in guilds while DMs work.", - "solution_summary": "Apply the configured proxy to all HTTP/REST requests (set undici's ProxyAgent/global dispatcher for REST and guild-resolve code paths) or add a global proxy configuration that installs a ProxyAgent for all network calls; ensure guild resolve uses the proxied path and add retry logic.", - "maintainer_signal_summary": "Regression: worked before but now Node.js built-in fetch/undici doesn't respect HTTP_PROXY, and current code only proxies the Gateway WS. A workaround (setGlobalDispatcher) fixes REST but not guild resolve, indicating a separate code path; tests and fixes should target REST/guild-resolve startup paths and consider a global proxy option.", - "dedupe_summary": "Discord channel proxy config only proxies the WebSocket gateway, not REST API calls or guild resolution, causing REST timeouts and unresolved guild channels behind an HTTP proxy." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (proxy applies to WebSocket but not REST/guild resolution) without boilerplate or irrelevant details, so it will cluster with similar reports." - }, - "usage": { - "input_tokens": 2335, - "output_tokens": 745 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "Outbound Discord REST requests (message sends/attachments) ignore the configured channels.discord.proxy and fail with TypeError: fetch failed, while the WebSocket gateway (receiving) works. The Carbon RequestClient/fetch implementation currently has no proxy support, so bots behind network proxies (e.g., China) cannot send messages or upload/download media.", - "solution_summary": "Add proxy support to the Discord REST client by making RequestClient/RequestHandler pass a proxy agent (HTTP/SOCKS/TUN) or honor channels.discord.proxy for all outgoing HTTP requests, including message sends and attachment uploads/downloads. Ensure REST fetches use the same proxy behavior as the gateway and provide config-driven proxy handling.", - "maintainer_signal_summary": "User reports logs indicate \"rest proxy enabled\" but REST requests still bypass the proxy; WebSocket receives function normally. Affected OpenClaw version 2026.2.26; change required in Carbon's RequestClient/fetch integration to attach a proxy agent or use a proxy-aware fetch implementation so REST traffic routes through the configured proxy.", - "dedupe_summary": "Discord REST requests bypass the configured channels.discord.proxy because the Carbon RequestClient/fetch has no proxy support, causing outgoing message sends and attachment transfers to fail behind proxies." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "The summary succinctly states the core bug and root cause with precise technical terms (channels.discord.proxy, Carbon RequestClient/fetch, REST requests) and no extraneous template or testing details, so it will cluster with similar proxy-related REST failures." - }, - "usage": { - "input_tokens": 1017, - "output_tokens": 747 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "When OpenClaw's Discord provider is configured to use an HTTP proxy it can successfully connect the gateway/WebSocket and fetch the application ID, but startup REST calls (native command deployment and bot identity fetch) consistently fail with 'fetch failed', leaving the provider half-working.", - "solution_summary": "Ensure all Discord REST requests use the same proxy-aware HTTP fetch/dispatcher as the gateway (unify transport/proxy handling), so startup REST calls are routed through the configured proxy and include consistent retry/error handling.", - "maintainer_signal_summary": "Logs show 'rest proxy enabled' and 'gateway proxy enabled', application ID fetch succeeds, but deploy-rest:put and fetch-bot-identity return 'fetch failed' despite 'logged in to discord'. Issue reproducible with channels.discord.proxy configured; OpenClaw versions 2026.3.12/2026.3.13 reported. Likely different code paths for gateway vs REST requests.", - "dedupe_summary": "Discord provider connects via proxy to the gateway but some startup REST calls (PUT /applications/.../commands and bot identity fetch) fail with 'fetch failed' — likely due to inconsistent or missing proxy handling for certain REST request paths." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that identifies the core failure (startup REST calls for command deployment and bot identity fetch failing with 'fetch failed' despite gateway login) and points to a likely cause (inconsistent/missing proxy handling) which is ideal for deduplication." - }, - "usage": { - "input_tokens": 1163, - "output_tokens": 713 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "Discord channel proxy is logged as enabled but Discord REST API calls fail with \"fetch failed\" and the gateway WebSocket never connects when running OpenClaw under WSL2 with an HTTP proxy configured (channels.discord.proxy). This renders the Discord channel non-functional despite the proxy being reachable via curl.", - "solution_summary": "Ensure the configured HTTP proxy is actually applied to the Discord.js REST/fetch client and WebSocket gateway (use a proxy agent or set fetch/HTTP client to honor channels.discord.proxy), so REST requests and gateway connections go through the proxy in WSL2 and similar environments.", - "maintainer_signal_summary": "Logs show \"rest proxy enabled\" and \"gateway proxy enabled\" but subsequent REST operations error with \"fetch failed\" and gatewayConnected=false; curl via the proxy succeeds and Feishu works, so the proxy itself is fine. Affects users requiring HTTP proxy (WSL2); reproducible and high severity; config stored at ~/.openclaw/openclaw.json → channels.discord.proxy.", - "dedupe_summary": "Discord proxy config is recognized but not actually applied to the Discord.js REST/fetch (and gateway) calls, causing \"fetch failed\" and gateway connection failures when an HTTP proxy is required (e.g., WSL2)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core problem — proxy config is detected but not applied to Discord.js REST/gateway calls causing 'fetch failed' — without extraneous template or repro details." - }, - "usage": { - "input_tokens": 1000, - "output_tokens": 525 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "Discord REST API calls from the OpenClaw gateway ignore channels.discord.proxy and HTTP(S)_PROXY environment variables and fail with 'TypeError: fetch failed', while the Discord WebSocket and other channels (e.g., Telegram) work correctly.", - "solution_summary": "Route Discord REST API requests through the same proxy-aware fetch/SSRF-guard path used for web_search/web_fetch so channels.discord.proxy and HTTP(S)_PROXY are honored; update the Discord channel code to use the shared proxy-aware fetch implementation.", - "maintainer_signal_summary": "Logs show 'discord final reply failed: TypeError: fetch failed'. User provided config, env vars, and reproduction steps; curl through the proxy to Discord API succeeds, and changelog notes web tools were fixed but Discord REST was excluded. User attempted both channel config and env vars and reports a webhook workaround only for push, not replies.", - "dedupe_summary": "OpenClaw's Discord REST API requests are not routed through the configured proxy/HTTP(S)_PROXY and fail with 'fetch failed'." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary stating that Discord REST requests aren't using the configured proxy (HTTP(S)_PROXY) and fail with 'fetch failed', which captures the core bug for deduplication." - }, - "usage": { - "input_tokens": 1023, - "output_tokens": 701 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "The CLI command openclaw agent --channel discord --to resolves and sends messages to the generic agent:*:main session instead of the live Discord channel/thread session because the session resolver does not look up sessions by delivery context (channel + to + accountId) and Discord targets are not normalized.", - "solution_summary": "Update the agent CLI session resolution to search by real delivery context (channel + to + accountId), normalize Discord target identifiers (e.g., prepend channel:), ensure the command path passes channel and replyAccount/accountId to the resolver, and fix compact resolver lookup logic to use the correct main-session builder.", - "maintainer_signal_summary": "Manual hotfixes were applied to compiled dist files (dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js) to add delivery-context lookup and normalization; changes are high-risk and will be lost on reinstall. Commit an equivalent fix in source, run post-update revalidation, and review the compact resolver and command-path propagation to avoid regressions.", - "dedupe_summary": "openclaw CLI agent --channel discord --to routes to agent:*:main instead of the live Discord channel/thread because the session resolver omits lookup by delivery context (channel+to+accountId) and Discord target normalization is missing." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that states the CLI command, observed misrouting to agent:*:main, and the precise root causes (missing delivery-context lookup and Discord target normalization), which is ideal for deduplication." - }, - "usage": { - "input_tokens": 950, - "output_tokens": 507 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions in OpenClaw's Discord integration: 1) volatile per-message metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) was being re-injected into thread sessions every turn, causing progressive context bloat and degraded behavior; 2) CLI agent commands targeted to a Discord thread routed to the default main session (agent:*:main) because the session resolver did not lookup by actual delivery context (channel+to+accountId).", - "solution_summary": "Prevent re-injection of specified volatile metadata for Discord threads; update CLI/gateway session resolution to normalize Discord targets and resolve sessions by delivery context (channel + to + accountId); pass channel and replyAccount/accountId into the resolver and replace incorrect resolver usage with buildAgentMainSessionKey(...).", - "maintainer_signal_summary": "Fixes were applied directly to compiled runtime bundles in openclaw/dist/ (pi-embedded, gateway-cli, compact, register.agent) and verified by restarting the gateway service and observing corrected writer/reasoning sessions and no repeated thread context; manual patching of dist/ files is required and will be lost on reinstall, so re-application script/update is needed.", - "dedupe_summary": "Discord thread sessions repeatedly receive volatile metadata each turn (message_id, reply_to_id, timestamp, Thread starter, chat-history) causing context bloat, and CLI agent commands route to the wrong session because the resolver doesn't resolve by delivery context (channel+to+accountId)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: captures the two core regressions (repeated injection of volatile Discord thread metadata causing context bloat, and CLI routing to wrong session due to resolver not using delivery context), with no boilerplate or irrelevant detail." - }, - "usage": { - "input_tokens": 869, - "output_tokens": 528 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "The sandbox media staging limit is hardcoded as MEDIA_MAX_BYTES = 5 * 1024 * 1024 inside dist/store-*.js, causing any files over 5MB to be rejected or silently fail when copying into sandboxed agents (sandbox.mode: \"non-main\").", - "solution_summary": "Make the MEDIA_MAX_BYTES value configurable via openclaw.json (e.g., tools.media.maxBytes) or honor existing per-type media limits before staging; remove the hardcoded constant in the distributed build and read the limit from runtime config with a sensible default.", - "maintainer_signal_summary": "Regression present since at least v2026.3.7 and still in v2026.3.8; users currently must patch dist/store-*.js after npm install to raise the cap (sed workaround). Affects npm-installed distribution and causes silent failures for >5MB files in sandboxed agents.", - "dedupe_summary": "MEDIA_MAX_BYTES hardcoded to 5MB in dist/store-*.js prevents staging files >5MB into sandboxed agents and there is no openclaw.json config to override it." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "The summary concisely and specifically states the core bug (hardcoded MEDIA_MAX_BYTES = 5MB in dist/store-*.js blocking sandbox file staging) and that there's no openclaw.json override, which is ideal for deduplication." - }, - "usage": { - "input_tokens": 916, - "output_tokens": 626 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "OpenClaw's Discord provider fails all REST API requests with \"fetch failed\" even though the REST proxy is configured and logged as enabled; the Gateway WebSocket works and the bot can receive messages but cannot send replies or deploy commands.", - "solution_summary": "Update the Discord REST client to actually use the configured proxy for fetch calls (e.g., attach an HTTP(S) proxy agent or honor proxy env vars) so REST requests are routed through the proxy like curl and the Gateway are.", - "maintainer_signal_summary": "Logs show \"rest proxy enabled\" and gateway proxy enabled, WebSocket connection and inbound messages work, and direct curl with the same proxy succeeds, but native command deployment and other REST calls fail with \"fetch failed\"; indicates the REST proxy implementation or fetch usage isn't honoring the proxy configuration.", - "dedupe_summary": "Discord REST API requests error with \"fetch failed\" despite rest proxy being enabled in OpenClaw; WebSocket works and curl with the proxy succeeds, implying the Discord provider's REST client isn't using the configured proxy and prevents sending messages." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies REST API calls failing with 'fetch failed' despite proxy config, notes WebSocket/curl workarounds, and pinpoints the likely cause (Discord REST client not using the proxy), which is ideal for deduplication." - }, - "usage": { - "input_tokens": 861, - "output_tokens": 751 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "Discord REST API calls made via @buape/carbon's RequestClient ignore the channels.discord.proxy setting in openclaw.json and call globalThis.fetch without a proxy-aware dispatcher; as a result REST requests (and other fetch paths like memorySearch and attachment downloads) bypass the configured HTTP proxy while the WebSocket gateway uses it.", - "solution_summary": "Change @buape/carbon's RequestClient (and other fetch code paths) to accept and use an explicit dispatcher/proxy option and have OpenClaw pass channels.discord.proxy into those REST/fetch calls, or set a proxy-aware undici dispatcher for native fetch so Node's fetch routes through the configured proxy.", - "maintainer_signal_summary": "Update RequestClient API to accept a dispatcher/proxy and wire OpenClaw channels.discord.proxy into REST calls; audit memorySearch and fetchRemoteMedia to use the same proxy handling; avoid using NODE_OPTIONS=--use-env-proxy as it breaks WebSocket connections.", - "dedupe_summary": "Discord REST API requests via @buape/carbon RequestClient bypass the configured channels.discord.proxy (globalThis.fetch used without proxy dispatcher), so REST/fetch traffic is not routed through the HTTP proxy while the WebSocket gateway is." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies @buape/carbon RequestClient using globalThis.fetch and bypassing channels.discord.proxy so REST calls aren't routed through the HTTP proxy, matching similar reports accurately." - }, - "usage": { - "input_tokens": 870, - "output_tokens": 781 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "OpenClaw degrades under sustained load with long-running conversations: context grows without bounds (writer ~42k tokens, reasoning ~90k) causing rising per-turn latency; long turns block Discord lanes and cause serialization under load; typing indicators can remain stuck until TTL in some code paths.", - "solution_summary": "Applied configuration limits and timeouts: reduced concurrency (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents maxConcurrent 8→4, maxChildrenPerAgent 8→4), set agents.defaults.contextTokens=80000, increased channels.discord.eventQueue.listenerTimeout 120000→300000, improved patching script to handle hash variants, and restarted the gateway.", - "maintainer_signal_summary": "Root causes are missing effective context token limits, overly high concurrency saturating lanes, and insufficient event queue timeouts; restart mitigates but does not eliminate the problem. Recommended long-term fixes: enforce/adapt context token caps, tighten concurrency controls, increase event-queue timeouts, and expose these as tunable config options.", - "dedupe_summary": "Performance degradation and Discord lane blocking during sustained long conversations caused by unbounded context token growth and excessive concurrency, leading to serialization and stuck typing indicators; remedy via context token limits, lower concurrency, and longer event-queue listener timeouts." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that names the root technical causes (unbounded context token growth, excessive concurrency) and observable impacts (Discord lane blocking, serialization, stuck typing), with suggested mitigations — no boilerplate and good for clustering similar issues." - }, - "usage": { - "input_tokens": 804, - "output_tokens": 862 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11, Discord thread handling repeatedly injects per-message volatile metadata (message_id, reply_to_id, timestamp, Thread starter, \"Chat history since last reply\") on every conversation turn, causing context contamination, token bloat, performance degradation, and prompt cache invalidation.", - "solution_summary": "Add an isNewSession guard so thread-starter/history and volatile IDs/timestamps are injected only on the first turn, correct session key generation to use buildAgentMainSessionKey(...), and implement the fix in source (not just compiled dist) with post-update validation.", - "maintainer_signal_summary": "A manual hotpatch was applied to compiled files (dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js) adding an isNewSession guard and replacing resolveAgentMainSessionKey with buildAgentMainSessionKey; this is high-risk because the changes live only in dist and will be lost on updates, so a source-level fix and automated revalidation are required.", - "dedupe_summary": "Discord threads re-inject per-message volatile metadata every turn (message_id, reply_to_id, timestamp, Thread starter, chat history) due to a missing isNewSession guard, causing context pollution and prompt cache busting." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that identifies the exact symptom (per-turn injection of volatile Discord thread metadata), root cause (missing isNewSession guard), and impact (context pollution and cache busting), making it ideal for deduplication." - }, - "usage": { - "input_tokens": 816, - "output_tokens": 829 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "Discord thread messages repeatedly have volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) re-injected on every turn, causing context duplication, degraded performance in long threads, increased token costs, and prompt-cache busting. The issue originates from the compiled runtime handling of Discord threads in dist/pi-embedded-D6PpOsxP.js which lacks a guard to only inject that metadata on the first turn.", - "solution_summary": "A manual patch to dist/pi-embedded-D6PpOsxP.js was applied to stop injecting volatile Discord thread metadata after the first turn, restoring prompt caching and reducing token usage; recommended permanent fix is to implement the guard in source code and add revalidation post-update. ", - "maintainer_signal_summary": "High-risk regression: current fix is a manual edit to compiled dist files and will be lost on updates; maintainers should apply the fix in source, add a test/guard to ensure metadata injection happens only on the first thread turn, and include this in CI to prevent future regressions. ", - "dedupe_summary": "Discord threads repeatedly re-inject per-message volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, duplicating context and busting prompt cache due to missing first-turn injection guard in the compiled runtime (dist/pi-embedded-*.js)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that names the exact metadata re-injected, the root cause (missing first-turn guard), and the compiled runtime file pattern, so it will accurately cluster similar reports." - }, - "usage": { - "input_tokens": 824, - "output_tokens": 637 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "Discord threads in the OpenClaw runtime exhibit two regressions: per-turn re-injection of message metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing progressive context growth and latency, and the session resolver falling back to agent:*:main instead of resolving an existing live thread-bound session, causing CLI commands to route to the wrong session.", - "solution_summary": "Applied runtime patches to compiled dist JS bundles to stop repeated metadata reinjection and to make session resolution use delivery context (channel + to + accountId); added idempotent re-apply script and backups, restarted and validated the gateway.", - "maintainer_signal_summary": "Fix was applied manually to compiled bundles (dist/*.js) and is non-persistent across installs—backups (*.bak-*) and an idempotent reapplication script exist; gateway restarted and logs verified; upstream permanent fix and regression tests should be implemented to avoid repeated manual patches.", - "dedupe_summary": "Discord-thread regression: per-turn repeated metadata injection causing unbounded context growth and latency, plus session resolver falling back to agent:*:main instead of resolving thread-bound sessions, resulting in wrong CLI/session routing." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it names the two technical regressions (per-turn metadata causing context growth/latency and session resolver falling back to agent:*:main instead of thread-bound sessions) with no boilerplate, so it will cluster correctly." - }, - "usage": { - "input_tokens": 800, - "output_tokens": 715 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "OpenClaw cannot send reply messages to Discord: sending fails with \"TypeError: fetch failed\" even though Discord proxy settings are enabled and the bot is logged in; receiving messages and model responses work fine.", - "solution_summary": "Ensure the Discord REST send-message path respects the configured proxy (use a proxy agent or honor HTTP_PROXY/HTTPS_PROXY for Node fetch) and surface the underlying fetch error; update fetch usage to attach the proxy agent or global proxy settings so replies route through the proxy.", - "maintainer_signal_summary": "Regression observed in 2026.3.2: logs show \"discord: rest proxy enabled\" and \"discord: gateway proxy enabled\" and successful login, but final reply fails with \"TypeError: fetch failed\"; curl through the same proxy succeeds, and the issue affects both DMs and group chats, indicating the REST send code likely isn't applying the proxy.", - "dedupe_summary": "OpenClaw cannot send Discord reply messages because the REST send path fails with \"fetch failed\" despite proxy being enabled and working elsewhere, indicating the message-send fetch call is not using the configured proxy." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that identifies the core failure (Discord REST send path failing with 'fetch failed' despite proxy config) and the likely cause (send call not using the proxy), with no template noise." - }, - "usage": { - "input_tokens": 740, - "output_tokens": 686 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "openclaw agent --channel discord --to ... routes messages to agent:*:main instead of the live Discord channel/thread session because the CLI session resolver does not lookup sessions by delivery context and does not normalize Discord target IDs.", - "solution_summary": "Modify the session resolver to normalize Discord targets (e.g., prepend channel:) and perform a session lookup by delivery context (channel + to + accountId) before falling back to agent:*:main; the reporter patched the compiled dist/gateway-cli-BjsM6fWb.js manually with this fix.", - "maintainer_signal_summary": "The bug is in the compiled artifact dist/gateway-cli-BjsM6fWb.js; a manual edit was made to dist to add normalization and delivery-context lookup, but this is high-regression risk and will be lost on reinstall—fix must be applied in source and released properly.", - "dedupe_summary": "CLI agent routing to Discord channels falls back to agent:*:main because the session resolver fails to normalize Discord target IDs and doesn't lookup sessions by delivery context (channel+to+accountId)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states the CLI routes to agent:*:main because the session resolver doesn't normalize Discord target IDs or lookup by delivery context (channel+to+accountId)." - }, - "usage": { - "input_tokens": 805, - "output_tokens": 708 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "A regression in OpenClaw v2026.3.11: the compiled file dist/compact-1mmJ_KWL.js calls a nonexistent function resolveAgentMainSessionKey, causing ReferenceError at runtime, breaking the agent command and potentially crashing the gateway when processing agent commands (session resolver patch introduced the wrong function).", - "solution_summary": "Manually patch dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...); this removes the ReferenceError and restores agent/session behavior. Note this is a high-risk fix because it modifies compiled files and will be lost on reinstall unless fixed in source. ", - "maintainer_signal_summary": "This is a regression introduced by the earlier session resolver fix (#3); maintainers should update the source patch to use buildAgentMainSessionKey instead of resolveAgentMainSessionKey so compiled bundles do not contain the invalid call. The issue is urgent because the manual fix in dist/ will be overwritten by updates. ", - "dedupe_summary": "Compiled bundle dist/compact-1mmJ_KWL.js calls undefined resolveAgentMainSessionKey instead of buildAgentMainSessionKey, causing ReferenceError and breaking agent commands in OpenClaw v2026.3.11; requires replacing the call in the compiled file or fixing the source patch." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary clearly states the concrete regression (wrong function call in compiled bundle causing ReferenceError and broken agent commands) but includes a version tag and a remediation note which are minor noise for deduping." - }, - "usage": { - "input_tokens": 776, - "output_tokens": 608 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "Discord plugin's REST API requests ignore the configured channels.discord.proxy (only WebSocket connections use it), causing health-checks to report 'fetch failed' and message sends to time out in environments that require an HTTP proxy.", - "solution_summary": "Ensure the Discord plugin's HTTP client uses channels.discord.proxy for all REST API requests and health checks so REST calls are routed through the configured proxy; add tests and update health-check logic to use the proxied client.", - "maintainer_signal_summary": "Regression observed in OpenClaw 2026.3.8 (commit 3caab92). Repro: set channels.discord.proxy, verify curl via proxy succeeds, then run openclaw message send or health check which fails with 'fetch failed' while WebSocket proxying works—indicating REST calls bypass the proxy.", - "dedupe_summary": "Discord channel REST API requests ignore channels.discord.proxy (only WebSocket uses the proxy), causing fetch failed/timeouts behind an HTTP proxy." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and focused: it states the core bug (REST API ignores channels.discord.proxy while WebSocket uses it) with technical terms and impact (fetch failures/timeouts), no boilerplate or extraneous detail." - }, - "usage": { - "input_tokens": 732, - "output_tokens": 724 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the compiled session resolver for the agent command (dist/compact-1mmJ_KWL.js) does not perform a delivery-context lookup (channel + to + accountId), so sessions fall back to agent:*:main instead of resolving to the live session for the target channel/thread. This breaks routing and causes agent responses to be associated with the wrong session.", - "solution_summary": "Implement delivery-context lookup in the agent command's session resolver: search for an existing session by channel + to + accountId and only fall back to agent:*:main when no specific session is found; apply the change in source code and rebuild (not just edit compiled dist files), and add tests to cover delivery-context resolution.", - "maintainer_signal_summary": "This is a production-impact bug in compiled code; the correct fix must be applied in the source and included in the build pipeline because manual edits to dist/compact-*.js are high-risk and will be overwritten by upgrades. Prioritize a source-level fix, add tests, and release a rebuild to prevent regressions.", - "dedupe_summary": "Agent command session resolver (dist/compact-*.js) lacks delivery-context lookup (channel+to+accountId), causing sessions to default to agent:*:main instead of the target channel/thread session." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies the session resolver in dist/compact-*.js missing delivery-context lookup (channel+to+accountId) and the resulting incorrect fallback to agent:*:main, with no boilerplate or noise." - }, - "usage": { - "input_tokens": 746, - "output_tokens": 646 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English (settings, buttons, agent/skill pages, channel configs, status messages, tooltips), creating usability barriers for non-English users—notably Chinese speakers; the distributed control-ui bundle contains these hardcoded strings.", - "solution_summary": "Extract all hardcoded UI strings into an i18n layer (e.g., JSON locale files), provide a zh-CN locale with translations, add a language selector or auto-detect system language, and build infrastructure to easily add more languages.", - "maintainer_signal_summary": "This is a feature request (priority medium-high) noting docs already have zh-CN translations and the author asks whether this is on the roadmap and if contributors are welcome; implementation requires changing control-ui bundles to use locale files.", - "dedupe_summary": "Add i18n to OpenClaw Control UI by extracting hardcoded English strings into locale files and adding Simplified Chinese (zh-CN) plus a language selector/auto-detect." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: summarizes extracting hardcoded UI strings into locale files, adding Simplified Chinese (zh-CN) and a language selector/auto-detect without any template noise." - }, - "usage": { - "input_tokens": 667, - "output_tokens": 595 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "WhatsApp channel's systemPrompt support was only at the root level, so group chats could not receive per-account or per-group systemPrompt instructions to customize reply behavior.", - "solution_summary": "Extend WhatsApp config/schema to support systemPrompt at channels.whatsapp, accounts., groups. (and wildcard '*'); implement resolution hierarchy where account-level overrides root, group-specific or '*' wildcard is chosen within the active groups map, account 'groups' fully replace root 'groups', and the final injected prompt is account prompt + resolved group prompt separated by a blank line; add tests and docs.", - "maintainer_signal_summary": "Code and config schema were changed for the WhatsApp channel to add account and group-level systemPrompt resolution, with account merging and group prompt composition implemented; account groups replace root groups; no changes to other channels, hot-reload behavior, or group allowlist/mention gating; tests and documentation included.", - "dedupe_summary": "Add hierarchical WhatsApp systemPrompt resolution: support systemPrompt at root, account, and group (including '*' wildcard) levels; account-level overrides root and account 'groups' replace root 'groups'; resolved prompt is account prompt plus group prompt." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific description of the new hierarchical WhatsApp systemPrompt resolution (root/account/group with wildcard and override/merge behavior) without template noise, so it will cluster correctly with similar changes." - }, - "usage": { - "input_tokens": 2443, - "output_tokens": 714 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before entering the inbound/agent processing path, and WhatsApp group policy resolution ignored account-scoped config, causing account-level group allowlist/fallbacks to be missed and valid group messages to be rejected.", - "solution_summary": "Enable append processing only for WhatsApp group messages (keep DM append skipped) and propagate accountId/account-scoped groupAllowFrom/allowFrom into WhatsApp group policy resolution so account-scoped settings are applied correctly.", - "maintainer_signal_summary": "Scoped bugfix touching WhatsApp inbound and group-activation code (no config/schema changes, DM append still skipped, no dist/debug noise); targeted tests and traces validate the fix; risk noted that enabling group append could replay backlog messages but mitigated by scope and dedupe.", - "dedupe_summary": "WhatsApp group messages sent as messages.upsert(type=\"append\") were skipped before agent processing, and group policy resolution omitted accountId so account-scoped groupAllowFrom/allowFrom were ignored resulting in valid group messages being rejected." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies both the messages.upsert(type=\"append\") handling bug and the missing accountId in group policy resolution that caused account-scoped allowFrom settings to be ignored, which accurately captures the core issue and impact." - }, - "usage": { - "input_tokens": 1995, - "output_tokens": 587 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because client IP resolution failed: resolveClientIp returned undefined for requests from a trusted proxy that omitted forwarding headers, and isLocalDirectRequest did not fall back to the raw socket address when allowRealIpFallback was enabled. This prevented local/direct auth detection and blocked Canvas for Serve users.", - "solution_summary": "Make resolveClientIp return the socket address as a fallback when no forwarding headers are present; update isLocalDirectRequest to check req.socket.remoteAddress when allowRealIpFallback is enabled; gate .ts.net host acceptance behind an existing allowTailscale flag and thread auth.allowTailscale through callers; add tests and docs describing required trustedProxies and allowRealIpFallback settings.", - "maintainer_signal_summary": "Includes unit tests and log traces showing the regression and fix, docs updated with required config, no new config keys introduced (allowTailscale already exists and defaults to true for tailscale.mode = \"serve\"), and notes about risks and mitigations; depends on an overlapping PR for allowTailscale gating.", - "dedupe_summary": "Canvas/A2UI 401 when behind Tailscale Serve due to client-IP resolution bug: resolveClientIp returns undefined for direct trusted-proxy requests lacking XFF/X-Real-IP and isLocalDirectRequest fails to fallback to req.socket.remoteAddress with allowRealIpFallback enabled; fixed by using socket-address fallback and gating .ts.net host acceptance behind allowTailscale." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the root cause (client IP resolution and fallback failures) and the fix (socket-address fallback and .ts.net gating) that would reliably cluster with similar Tailscale/Trusted-proxy IP issues." - }, - "usage": { - "input_tokens": 1834, - "output_tokens": 765 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "checkInboundAccessControl() computed an isSelfChat heuristic but did not enforce it, allowing non-self WhatsApp messages (group messages, DMs from strangers, broadcasts) to pass through because group policy resolution defaulted to \"open\". This violates the intended self-chat isolation for accounts configured with explicit selfChatMode: true and lets unwanted messages reach downstream processing.", - "solution_summary": "Add an early-return guard in checkInboundAccessControl that rejects all non-self messages when account.selfChatMode === true, placed before any group/DM policy evaluation; the guard checks the explicit config flag (not the heuristic) and preserves self-messages via isSamePhone. No other channel access control or downstream policy logic was changed.", - "maintainer_signal_summary": "Change is small and focused to src/web/inbound/access-control.ts, tests pass (existing access-control tests), and the guard can be reverted quickly if needed. The fix intentionally does not apply to auto-detected self-chat heuristic to avoid blocking teammates; fail-closed behavior occurs if selfE164 is null.", - "dedupe_summary": "WhatsApp self-chat mode (account.selfChatMode === true) did not block non-self messages because isSelfChat was computed but unused and groupPolicy defaulted to \"open\"; fix adds an early-return in WhatsApp inbound access control to reject all non-self messages before policy evaluation." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states the bug (self-chat mode failed to block non-self messages due to an unused isSelfChat and default groupPolicy) and the fix (early-return guard in inbound access control), making it ideal for deduplication." - }, - "usage": { - "input_tokens": 1549, - "output_tokens": 652 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "With groupPolicy set to \"allowlist\", group messages from senders not in groupAllowFrom are dropped at the access-control stage and never reach the mention-gating/context-storage logic, so the agent cannot see those messages as recent context.", - "solution_summary": "Add an opt-in config flag groupContextFromAll (top-level and per-account, default false). When enabled, access-control will mark non-allowlisted group messages as storeForContext, the inbound monitor will mark them contextOnly and skip read receipts, and they will be recorded as pending group context without triggering replies.", - "maintainer_signal_summary": "Opt-in and backward-compatible (default false); changes localized to WhatsApp inbound pipeline, types, config schema, and auto-reply monitor; tested locally. Potential edge cases around allowlist/phone-number normalization and bypassing existing group gating/state updates that may cause subtle behavior differences.", - "dedupe_summary": "Introduce groupContextFromAll to preserve group messages from non-allowlisted senders as contextOnly/pending context (visible in recent chat context) instead of dropping them when groupPolicy is \"allowlist\"; context-only messages cannot trigger replies and skip read receipts." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific description of the new feature (groupContextFromAll) and its behavior (preserve non-allowlisted group messages as context-only that can't trigger replies and skip read receipts), suitable for deduplication." - }, - "usage": { - "input_tokens": 1566, - "output_tokens": 747 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "sessions_spawn always created a new random-UUID sub-agent session, preventing deterministic reuse of a sub-agent's conversation history and risking cross-agent session injection if an external session identifier were allowed unchecked.", - "solution_summary": "Add an optional sessionKey parameter to sessions_spawn that, when provided, maps short keys to a namespaced session id (agent:{agentId}:subagent:{key}), accepts fully-qualified session ids only if their embedded agentId matches the target agent, and falls back to crypto.randomUUID() when omitted; includes tests for deterministic keys, UUID fallback, fully-qualified passthrough, and mismatch rejection.", - "maintainer_signal_summary": "Tests currently contain a broken import (openclaw-tools.js vs clawdbot-tools.js) that prevents compilation; cross-agent validation has a case-sensitivity bug where embedded agentId is not normalized causing false mismatches; error handling is inconsistent (throws vs returning a forbidden jsonResult), which may alter caller behavior.", - "dedupe_summary": "Add optional sessionKey to sessions_spawn to enable deterministic sub-agent sessions (short keys namespaced to agent:{agentId}:subagent:{key}, fully-qualified keys validated to match target agentId), with fallback to random UUID." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change (optional sessionKey, namespacing rules, validation, and UUID fallback) without boilerplate, so it will match similar PRs/issues." - }, - "usage": { - "input_tokens": 1619, - "output_tokens": 556 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) fails because the Telegram adapter calls editMessageText, causing a 400 error 'there is no text in the message to edit' and leaving message(action=edit) requests failing for captionable messages. Affected component: src/telegram/send.ts (editMessageTelegram).", - "solution_summary": "editMessageTelegram now catches the specific 400 error (NO_TEXT_IN_MESSAGE_RE) and retries using editMessageCaption for media messages; buttons-only updates use editMessageReplyMarkup directly; MESSAGE_NOT_MODIFIED is treated as success and HTML parse fallback preserved. Six unit tests were added to cover retry, buttons-only, error propagation, and MESSAGE_NOT_MODIFIED handling.", - "maintainer_signal_summary": "Change is backward-compatible with no interface or config changes and only affects the Telegram channel adapter; six new unit tests added; risk that the regex may not match if Telegram changes the error wording (unmatched errors will continue to propagate as before). Revert by restoring src/telegram/send.ts.", - "dedupe_summary": "Telegram edit fails for media messages because adapter calls editMessageText (400: 'there is no text in the message to edit'); fix by detecting that error and retrying with editMessageCaption (use editMessageReplyMarkup for buttons-only)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary that clearly states the Telegram API error when editing media captions and the adapter change to detect the 400 and retry with editMessageCaption (and use editMessageReplyMarkup for buttons-only), so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 1500, - "output_tokens": 736 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "WhatsApp integration was hardcoded to disable block streaming (disableBlockStreaming: true), causing all messages to be batched and sent only after the agent finished even when per-account or global block streaming settings requested progressive delivery. This broke expected streaming behavior for users who enabled block streaming.", - "solution_summary": "Restore config-driven block streaming by reading per-account channels.whatsapp.blockStreaming and falling back to agents.defaults.blockStreamingDefault; adjust the deliver callback to only suppress tool payloads (reasoning payloads remain filtered upstream by shouldSuppressReasoningPayload).", - "maintainer_signal_summary": "Change touches extensions/whatsapp message delivery logic to respect per-account/global blockStreaming settings and limits suppression in the deliver callback to tool payloads; reasoning suppression is unchanged and upstream filtering still prevents leaking thinking content. Tests and live verification show progressive delivery restored and no reasoning/tool leaks; default behavior remains off unless enabled.", - "dedupe_summary": "WhatsApp integration ignores blockStreaming config and batches messages because disableBlockStreaming was hardcoded; needs to be made config-driven so per-account/global blockStreaming settings enable progressive block streaming." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states that WhatsApp ignored blockStreaming due to a hardcoded disableBlockStreaming and must be made config-driven to respect per-account/global settings and enable progressive streaming." - }, - "usage": { - "input_tokens": 1492, - "output_tokens": 407 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Sending video/media via the Telegram channel could cause the gateway to hang and become completely unresponsive. Root cause: content-length checks triggered before consuming the HTTP response body, leaving an unconsumed body that causes undici Agent.close()/closeDispatcher to deadlock.", - "solution_summary": "Increase default channels.telegram.mediaMaxMb from 5 to 20; fix closeDispatcher to impose a timeout and fallback to destroy the agent to avoid deadlock; make media download failures non-fatal (still forward text/captions); add timeouts and retry logic to getFile and download operations; strip inbound media path metadata from UI.", - "maintainer_signal_summary": "Changes affect gateway/orchestration and Telegram integration (not SSRF policy): key files src/infra/net/ssrf.ts and src/telegram/bot.ts; backward compatible and configurable via channels.telegram.mediaMaxMb; tests added and verified on live bot; risk: 3s closeDispatcher timeout may prematurely destroy connections but is reversible and default limit can be lowered to 5 for rollback.", - "dedupe_summary": "Gateway hangs when receiving large Telegram media because content-length rejection leaves the response body unconsumed, causing undici Agent.close()/closeDispatcher to block; fix implemented by raising mediaMaxMb, adding timeout+destroy fallback for dispatcher close, and making media download errors non-fatal with retries." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific description of the root cause (unconsumed response body causing undici Agent/dispatcher deadlock) and the implemented fixes (increase media limit, timeout/destroy fallback, non-fatal downloads with retries), so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 1588, - "output_tokens": 870 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "Telegram integration fallback body text used a hardcoded '' placeholder for non-audio media when no caption/text was present, causing document (PDF/DOCX) and other non-photo attachments to be misidentified to agents.", - "solution_summary": "Change resolveTelegramInboundBody to use the already-computed, type-aware placeholder variable so the fallback emits '', '', etc., and add unit tests verifying placeholders and media handling for document messages.", - "maintainer_signal_summary": "Small, scoped bugfix in extensions/telegram: only the agent-facing placeholder text was changed; media download and extraction logic unchanged. New unit tests were added; watch for downstream code that pattern-matches on '' in this narrow fallback case.", - "dedupe_summary": "Telegram inbound body fallback incorrectly emitted '' for document and other non-photo media when no caption/text was present; needs to use the type-aware ''/'' placeholder instead." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical, and focused on the core bug and intended fix (use type-aware media placeholder instead of hardcoded ), with no boilerplate or extraneous details." - }, - "usage": { - "input_tokens": 1415, - "output_tokens": 430 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "MEDIA_MAX_BYTES was hardcoded (5 MB) in src/media/store.ts so users cannot change the inbound media staging limit; files larger than 5 MB silently fail to stage into sandboxed agents, preventing processing of common large files like PDFs and high-res images.", - "solution_summary": "Introduce a new optional config field media.maxBytes (integer, default 5 MB, max 500 MB) and replace the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes() usage; the gateway reads media.maxBytes at boot, error messages now report the configured limit, and default behavior remains 5 MB when unset.", - "maintainer_signal_summary": "Change preserves backward compatibility (default 5 MB and MEDIA_MAX_BYTES exported as deprecated), schema enforces a 500 MB cap, tests pass (56 unit tests), bot review threads were addressed, and the change can be reverted by removing media.maxBytes from config.", - "dedupe_summary": "Hardcoded 5MB media staging limit prevents staging files >5MB into sandboxed agents; add a configurable media.maxBytes (default 5MB, cap 500MB) and replace the constant with runtime-configurable getters/setters." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core problem (hardcoded 5MB staging limit) and the precise change (add media.maxBytes config and replace constant with runtime getters/setters), so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 1536, - "output_tokens": 631 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "Telegram integration was silently dropping messages when media download failed (due to exceeding mediaMaxMb or Telegram Bot API limits) because the handler returned early; as a result the agent never received the message or ack and users got no indication their file/caption was lost. The affected component is the Telegram bot message handling (bot-handlers.ts), causing complete message loss including captions and text when downloads failed.", - "solution_summary": "On download failure, extract file metadata (name, size, type) from the Telegram message object, build a metadata stub and inject it into msg.text, and allow the message to continue through the normal pipeline instead of returning early; this lets the agent see warnings like \"⚠️ [File: ...] (exceeds 5MB download limit)\" and respond accordingly.", - "maintainer_signal_summary": "Bugfix in src/telegram/bot-handlers.ts for the Telegram integration; touches error paths for media downloads only (no change to normal map to SignalAccountConfig and Zod schema; wire resolveChannelGroupPolicy into the Signal event handler and commandGate authorizers so listed groups bypass sender-level checks and are authorized for control commands; update docs.", - "maintainer_signal_summary": "Implementation follows existing patterns from other channels, addresses Greptile review feedback (ensuring allowed groups can run commands), passes TypeScript checks, supersedes PR #4337, and fixes issue #25540; safe to merge with low risk.", - "dedupe_summary": "Signal channel: add channels.signal.groups. allowlist so listed groups bypass sender-level groupAllowFrom and are authorized for messages and control commands." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that names the specific config key and clearly states the functional change (group-level allowlist bypassing sender-level checks for messages and commands)." - }, - "usage": { - "input_tokens": 1130, - "output_tokens": 641 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked group-level allowlist support: when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter and never matches a group ID.", - "solution_summary": "Added a groups config to SignalAccountSchemaBase and invoked resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom; added four tests to cover group-level policy resolution.", - "maintainer_signal_summary": "Small Signal-specific fix (schema + event-handler change) with 4 new tests; all config tests pass and it resolves silent-dropping of group messages under groupPolicy=\"allowlist\" (fixes the reported issue).", - "dedupe_summary": "Signal ignores group-level allowlists: group IDs placed in groupAllowFrom are treated as sender identifiers so group messages are dropped; missing channels.signal.groups config and resolveChannelGroupPolicy call caused the bug." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: explains that Signal treated group IDs as sender IDs causing dropped group messages and notes the missing channels.signal.groups config/resolveChannelGroupPolicy, which is exactly the core bug for deduplication." - }, - "usage": { - "input_tokens": 930, - "output_tokens": 657 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "WhatsApp channel always coalesces agent output into a single final message because process-message.ts hardcodes disableBlockStreaming: true, so intermediate blocks (tool outputs/sleeps) are not delivered as separate messages.", - "solution_summary": "Make WhatsApp respect the channels.whatsapp.blockStreaming config by adding resolveWhatsAppBlockStreamingEnabled(), replacing the hardcoded flag with the config-driven value, treating intermediate blocks as valid replies when blockStreaming is enabled, registering the capability and default in the channel dock, and updating tests.", - "maintainer_signal_summary": "Change is small and backwards-compatible (default behavior remains coalescing when blockStreaming is false/omitted), tests updated to cover enabled/disabled behavior, registers new capability and default constant in dock, and fixes queuedFinal logic to match Telegram/Line behavior; labeled channel: whatsapp-web, app: web-ui, agents, size: S.", - "dedupe_summary": "WhatsApp channel hardcodes disableBlockStreaming:true causing all message blocks to be batched; should instead respect channels.whatsapp.blockStreaming so intermediate blocks can be delivered as separate WhatsApp messages when enabled." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the bug and intended config-driven change without boilerplate or extraneous details, so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 680, - "output_tokens": 456 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v3-examples.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v3-examples.json deleted file mode 100644 index d9d7474..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v3-examples.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v3-examples", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "Summarize this GitHub issue or pull request for duplicate detection clustering. The dedupe_summary will be embedded as a vector — it must be clean signal with zero noise.\n\nREMOVE all template noise. Examples of what to strip:\n- \"### Bug type\\nRegression (worked before, now fails)\" → ignore, it's a template field\n- \"### Steps to reproduce\\n1. Go to settings\\n2. Click...\" → ignore reproduction steps\n- \"### Expected behavior\\nIt should work\" → ignore\n- \"- [x] Tests added\\n- [x] Docs updated\" → ignore checklist\n- \"### Environment\\nOS: macOS 14.2, Node 20.1\" → ignore unless the OS IS the bug\n- \"Fixes #1234\" → ignore cross-references\n- \"## Testing\\nRun `pnpm test`...\" → ignore testing instructions\n\nKEEP the core signal:\n- \"Discord REST API ignores proxy configuration\" → THIS is the signal\n- \"WhatsApp group messages not delivered when streaming enabled\" → THIS is the signal\n- \"Telegram media download hangs on large video files\" → THIS is the signal\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nPlain text, no markdown, 1-3 sentences each.\ndedupe_summary must be a specific, searchable description of the core issue or change.", - "timestamp": "2026-03-30T16:46:19.491Z", - "aggregate": { - "avg_score": 4.95, - "avg_signal_density": 4.95, - "boilerplate_count": 0, - "boilerplate_pct": 0, - "would_cluster_correctly_pct": 100, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 49863, - "total_output_tokens": 27548 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "channels.discord.proxy only applies to the Discord WebSocket gateway; REST API calls at startup (deploy commands, fetch bot identity) and guild resolution bypass the configured HTTP proxy and time out, leaving guild channels unresolved while DMs work.", - "solution_summary": "Apply the configured proxy to the undici HTTP dispatcher used for all Discord REST calls and guild resolve paths (not just the WebSocket), or add a global proxy config that sets a ProxyAgent; add retries for guild resolution.", - "maintainer_signal_summary": "Reproducible behind an HTTP proxy: WebSocket connects via proxy and DMs work but startup REST calls fail with \"fetch failed\" and logs show channels unresolved:guild:. setGlobalDispatcher(ProxyAgent) fixes REST calls but guild resolve still uses an unproxied path and remains unresolved.", - "dedupe_summary": "channels.discord.proxy only proxies the Gateway WS; REST API startup calls (deploy-rest, fetch-bot-identity) and guild resolve bypass proxy causing fetch failed timeouts; undici/Node fetch ignores HTTP_PROXY and a global ProxyAgent is needed; guild resolve uses a separate unproxied code path." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that identifies the core regression (only Gateway WS proxied while REST startup calls and guild-resolve bypass the proxy) and the likely root cause (undici/Node fetch ignoring HTTP_PROXY requiring a global ProxyAgent), so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 2382, - "output_tokens": 554 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "Discord bot receives messages over the gateway via proxy but cannot send messages because outbound REST requests fail with \"fetch failed\". channels.discord.proxy is logged as enabled but Carbon's RequestClient.fetch does not use the proxy, so REST API calls bypass the proxy and fail in restricted networks.", - "solution_summary": "Update RequestClient.fetch to honor channels.discord.proxy by routing REST requests through the configured proxy (support HTTP/SOCKS). Apply the proxy consistently to all Discord REST endpoints including attachment uploads/downloads, add tests and config validation, and integrate a proxy agent or env-based proxy support.", - "maintainer_signal_summary": "Reproduced on OpenClaw 2026.2.25/2026.2.26 (macOS) in a China environment requiring a proxy; logs show REST proxy enabled yet REST sends fail with TypeError: fetch failed. Root cause: Carbon RequestClient.fetch lacks proxy handling and so bypasses configured proxy for REST/attachment requests.", - "dedupe_summary": "channels.discord.proxy not applied to REST API sends because Carbon RequestClient.fetch lacks proxy support, causing fetch failed for outbound Discord REST requests while WebSocket gateway works." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies channels.discord.proxy not being applied to REST sends due to Carbon RequestClient.fetch lacking proxy support, and contrasts REST failure with working WebSocket, making it ideal for clustering similar reports." - }, - "usage": { - "input_tokens": 1064, - "output_tokens": 760 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "When OpenClaw is configured behind an HTTP proxy, the Discord provider can log in and the gateway works but some startup REST calls (native command deployment and bot identity fetch) consistently fail with 'fetch failed', leaving the provider half-functional.", - "solution_summary": "Unify proxy handling so all Discord REST requests use the same proxy-aware fetch/HTTP dispatcher as the gateway/login path; propagate the configured proxy to the REST client used for deploy commands and bot identity fetch (or use a shared HTTP agent).", - "maintainer_signal_summary": "Logs show rest proxy enabled and applicationId fetch succeeds, gateway proxy enabled and Discord logs in, but deploy-rest PUT to /applications/:id/commands and fetch-bot-identity both error with 'fetch failed'; issue repeats on health-monitor restarts. Suspected cause: some REST request paths are not using the configured proxy or the same HTTP transport as the gateway/WebSocket path.", - "dedupe_summary": "Discord provider logs in via proxy but startup REST calls (deploy native commands and fetch bot identity) fail with 'fetch failed' due to inconsistent/missing proxy handling for some REST request paths." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that states the core problem (gateway login succeeds via HTTP proxy while specific startup REST calls — native command deployment and bot identity fetch — fail due to inconsistent/missing proxy handling), so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 1210, - "output_tokens": 767 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "OpenClaw logs show the Discord proxy as enabled but REST API calls fail with 'fetch failed' and the gateway WebSocket never connects (gatewayConnected=false) when running under WSL2 with an HTTP proxy configured.", - "solution_summary": "Fix the Discord REST client to actually use the configured HTTP proxy (apply proxy agent or ensure node fetch used by Discord.js respects channels.discord.proxy) so REST calls succeed and the bot can authenticate and establish the gateway WebSocket.", - "maintainer_signal_summary": "OpenClaw 2026.3.13 on WSL2 (Windows 11) with channels.discord.proxy=\"http://127.0.0.1:10808\"; curl via the proxy to Discord API returns 200, OpenClaw logs 'discord: rest proxy enabled' and 'discord: gateway proxy enabled' but REST calls fail ('deploy-rest:put:error ... error=fetch failed', 'failed to fetch bot identity: TypeError: fetch failed') and gatewayConnected stays false; reproducible when a proxy is required; Feishu channel works in same environment.", - "dedupe_summary": "Discord proxy setting is logged but Discord.js REST/fetch ignores the proxy, causing 'fetch failed' errors and failed bot authentication under WSL2 with channels.discord.proxy set." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states that the configured Discord proxy is logged but ignored by Discord.js REST/fetch, causing 'fetch failed' errors and failed bot authentication, which will cluster correctly with similar proxy/REST issues." - }, - "usage": { - "input_tokens": 1047, - "output_tokens": 698 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "OpenClaw's Discord REST API calls ignore the configured proxy and fail with \"TypeError: fetch failed\" while the Discord WebSocket connection and other channels (e.g., Telegram) work through the proxy; curl to the Discord REST endpoint via the same proxy succeeds.", - "solution_summary": "Route Discord REST API HTTP calls through the same proxy-aware fetch/SSRF guard path used for web_search/web_fetch so they honor channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY and avoid the fetch failures.", - "maintainer_signal_summary": "OpenClaw 2026.2.26 on macOS; logs show \"discord final reply failed: TypeError: fetch failed\". channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY/ALL_PROXY set but REST calls still fail; curl via proxy to Discord API succeeds; WebSocket and Telegram REST use the proxy correctly. Changelog note: web tools were fixed in 2026.2.26 but Discord REST was not included.", - "dedupe_summary": "Discord REST API client in OpenClaw ignores configured proxy/HTTP(S)_PROXY and fails with \"TypeError: fetch failed\", while WebSocket and other channels work and curl via the proxy succeeds." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "The summary concisely and specifically states that OpenClaw's Discord REST client ignores proxy settings (including HTTP(S)_PROXY) and fails with 'TypeError: fetch failed', contrasting with working WebSocket and curl via the proxy, which is the core signal needed for deduplication." - }, - "usage": { - "input_tokens": 1070, - "output_tokens": 740 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "The CLI command openclaw agent --channel discord --to delivers messages to agent:*:main instead of the live Discord channel/thread session because the session resolver never searched by the real delivery context (channel + to + accountId), command path didn’t propagate channel/account info, and Discord targets were not normalized.", - "solution_summary": "Manual patches were applied to compiled dist files to add lookup by delivery context, normalize Discord targets (e.g., 148... → channel:148...), pass channel and replyAccount/accountId through the command path, and fix the compact resolver to use buildAgentMainSessionKey; after the patch the CLI routes to the correct live session.", - "maintainer_signal_summary": "Affected compiled files: dist/gateway-cli-BjsM6fWb.js, dist/compact-1mmJ_KWL.js, dist/register.agent-DHoJWl4M.js. Changes introduce high regression risk because they modify dist files directly; recommend implementing the fix in source, adding normalization and delivery-context lookup in resolver logic, and revalidating after updates.", - "dedupe_summary": "openclaw CLI 'agent --channel discord --to' incorrectly routes to agent:*:main because the session resolver lacks delivery-context lookup (channel+to+accountId) and does not normalize Discord targets; patched dist files add delivery-context lookup and target normalization." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (CLI routing wrong session) and root causes (missing delivery-context lookup and Discord target normalization) without extraneous template text." - }, - "usage": { - "input_tokens": 997, - "output_tokens": 760 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions in Discord handling: volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) was re-injected every turn causing progressive context degradation in threads; CLI agent commands resolved to agent:*:main instead of the live thread session, delivering to the wrong session.", - "solution_summary": "Prevented injection of volatile Discord thread metadata and updated session resolution to use actual delivery context by normalizing Discord targets and passing channel plus replyAccount/accountId to the resolver; replaced incorrect resolveAgentMainSessionKey usage with buildAgentMainSessionKey(...). Changes applied to compiled runtime bundles in dist (pi-embedded, gateway-cli, compact, register.agent).", - "maintainer_signal_summary": "Fixes were applied directly to compiled bundles in openclaw/dist and require re-application after updates; openclaw-gateway.service was restarted to verify writer/reasoning sessions show proper toolCall output and Discord threads no longer load repeated context. CLI now routes by actual Discord target instead of defaulting to main.", - "dedupe_summary": "Discord threads: repeated injection of volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) every turn causing context bloat; CLI resolver defaulted to agent:*:main instead of resolving by channel+to+accountId — fixed by blocking metadata re-injection and normalizing delivery context in the session resolver." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "The summary concisely captures the two technical regressions (volatile metadata re-injection causing context bloat and CLI session resolver falling back to agent:*:main instead of using channel+to+accountId) without boilerplate, so it will correctly cluster similar issues." - }, - "usage": { - "input_tokens": 916, - "output_tokens": 659 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "Sandbox media staging cap is hardcoded to 5 MB (const MEDIA_MAX_BYTES = 5 * 1024 * 1024 in dist/store-*.js), so files larger than 5 MB are rejected or silently fail when copied into sandboxed agents.", - "solution_summary": "Expose MEDIA_MAX_BYTES as a configurable value read from openclaw.json (for example tools.media.maxBytes) with a 5 MB default, and update store code to use that config before per-type checks.", - "maintainer_signal_summary": "Constant MEDIA_MAX_BYTES is defined in dist/store-*.js and has been hardcoded since at least v2026.3.7 (still present in v2026.3.8). Reproduce by running sandbox.mode: \"non-main\" and sending a file >5 MB. Temporary workaround: sed-patch dist/store-*.js after npm install. Suggested config key: tools.media.maxBytes; per-type keys tools.media.image.maxBytes and tools.media.audio.maxBytes already exist.", - "dedupe_summary": "Sandbox media staging limit MEDIA_MAX_BYTES hardcoded to 5 MB in dist/store-*.js and not configurable via openclaw.json, causing files >5MB to be rejected." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core issue (hardcoded MEDIA_MAX_BYTES in dist/store-*.js, not configurable via openclaw.json, causing >5MB files to be rejected) with no extraneous boilerplate." - }, - "usage": { - "input_tokens": 963, - "output_tokens": 813 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "Discord bot receives messages via Gateway but all Discord REST API requests fail with \"fetch failed\" even though proxy is configured and logs show \"rest proxy enabled\"; curl using the same proxy succeeds and Telegram works with the same proxy.", - "solution_summary": "Fix the Discord REST client to honor the configured HTTP proxy (pass a proxy-aware agent or use a proxy-capable fetch), ensure the REST proxy code path isn't being skipped, and add tests/logging to validate REST requests use the proxy.", - "maintainer_signal_summary": "Gateway logs indicate \"rest proxy enabled\" and WebSocket (Gateway) connections work; REST operations (deploy commands, fetch bot identity, sending messages) return TypeError: fetch failed; direct curl requests through the same proxy succeed, isolating the failure to the app's Discord REST proxy path.", - "dedupe_summary": "Discord REST API requests fail with \"fetch failed\" despite rest proxy enabled and the same proxy working via curl" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core issue—REST API calls erroring with 'fetch failed' despite proxy being enabled and usable via curl—no boilerplate or noisy details." - }, - "usage": { - "input_tokens": 908, - "output_tokens": 711 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "Discord REST API calls made via @buape/carbon's RequestClient (and related fetch uses like memorySearch and attachment downloads) ignore the channels.discord.proxy setting in openclaw.json; only the WebSocket gateway respects the proxy. RequestClient calls globalThis.fetch without a proxy-aware dispatcher, and Node.js 22's native fetch does not read HTTP(S)_PROXY env vars by default.", - "solution_summary": "Change @buape/carbon's RequestClient to accept an optional dispatcher/proxy configuration and have OpenClaw pass channels.discord.proxy to it so REST API calls use the same proxy as the WebSocket. As a temporary workaround, preload an undici EnvHttpProxyAgent via NODE_OPTIONS to set a global dispatcher, but avoid using --use-env-proxy because it breaks WebSocket connections.", - "maintainer_signal_summary": "RequestClient currently invokes globalThis.fetch with no proxy dispatcher, causing REST requests, memorySearch, and some media downloads to bypass configured proxy; fix by exposing/accepting a dispatcher or proxy option and forwarding openclaw.json channels.discord.proxy to it. Do not recommend NODE_OPTIONS=--use-env-proxy because it interferes with WebSocket upgrades.", - "dedupe_summary": "@buape/carbon RequestClient uses globalThis.fetch without a proxy-aware dispatcher, so Discord REST requests ignore openclaw.json channels.discord.proxy (Node.js fetch doesn’t honor env proxy by default); add dispatcher/proxy support to RequestClient." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and technical: it states that @buape/carbon's RequestClient uses globalThis.fetch (which doesn't respect proxy env) and causes Discord REST calls to ignore channels.discord.proxy, recommending dispatcher/proxy support." - }, - "usage": { - "input_tokens": 917, - "output_tokens": 686 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "OpenClaw shows performance degradation under sustained load with long-running conversations: unbounded context token growth (writer ~42k tokens, reasoning ~90k), Discord lane blocking causing serialization under load, and typing indicators stuck until TTL in some code paths. High concurrency settings and insufficient event queue timeouts exacerbate the issue.", - "solution_summary": "Reduced concurrency (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents maxConcurrent 8→4, maxChildrenPerAgent 8→4), set agents.defaults.contextTokens to 80000, and increased channels.discord.eventQueue.listenerTimeout from 120000 to 300000; improved patching script to cover all hash variants. Gateway restarted and errors (lane wait exceeded, typing TTL) ceased, though restarts are not a complete fix under sustained load.", - "maintainer_signal_summary": "Key observables: session token counts grew to ~42k (writer) and ~90k (reasoning), lane blocking and serialization by agentId under load, and typing indicators stuck until TTL. Applied config changes and restart reduced errors; recommend making context/concurrency limits adaptive or exposing them in openclaw config.", - "dedupe_summary": "Unbounded conversation context token growth plus high concurrency causes lane blocking/serialization and sticky typing indicators; mitigated by lowering maxConcurrentSessions/agent/subagent limits, setting agents.defaults.contextTokens=80000, and increasing Discord eventQueue listenerTimeout to 300000." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the root causes (unbounded context growth and high concurrency causing lane blocking/typing issues) and the applied mitigations, making it well-suited for clustering similar reports." - }, - "usage": { - "input_tokens": 851, - "output_tokens": 593 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11, Discord threads re-inject volatile metadata on every conversation turn (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply), causing context contamination, token growth, degraded performance, and prompt cache invalidation.", - "solution_summary": "Implement the fix in source: limit injection of thread-starter and volatile metadata to the first turn via an isNewSession guard, replace resolveAgentMainSessionKey with buildAgentMainSessionKey where needed, add tests and post-update validation, and rebuild so compiled dist no longer re-injects metadata.", - "maintainer_signal_summary": "Affected compiled files: dist/pi-embedded-D6PpOsxP.js (metadata injection) and dist/compact-1mmJ_KWL.js (missing isNewSession guard and wrong session-key call). Apply fix in source to stop per-turn injection of message_id/reply_to_id/timestamp/Thread starter/Chat history, add isNewSession guard, switch to buildAgentMainSessionKey(...), add tests and a post-update revalidation script; current manual dist patch is high-risk and will be overwritten on updates.", - "dedupe_summary": "Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution and prompt-cache bust; compiled files dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js inject metadata and lack an isNewSession guard." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that states the core bug (Discord threads re-inject volatile metadata every turn), lists the exact fields affected and implicated compiled files, and notes the missing session guard—suitable for clustering similar reports." - }, - "usage": { - "input_tokens": 863, - "output_tokens": 882 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, repeated prompt content, degraded performance on long threads, and prompt-cache busting that increases token costs.", - "solution_summary": "A manual patch was applied to the compiled artifact dist/pi-embedded-D6PpOsxP.js to stop re-injecting the listed metadata after the first turn; validation shows repeated context stopped, prompt caching restored, and token usage reduced. Recommend implementing the fix in source and adding a post-update revalidation to avoid losing the change on upgrades.", - "maintainer_signal_summary": "Affected file: dist/pi-embedded-D6PpOsxP.js; root cause is a missing guard that limits Discord thread metadata injection to the first turn (unlike Slack/iMessage). High regression risk because the fix is applied to compiled files—priority: commit source-level fix, add tests, and ensure upgrade-safe deployment or revalidation script.", - "dedupe_summary": "Discord threads re-inject message_id, reply_to_id, timestamp, Thread starter, and Chat history on every turn in v2026.3.11; patched in dist/pi-embedded-D6PpOsxP.js to inject this metadata only on the first turn." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary clearly states the core bug and lists the exact metadata re-injected (good for clustering), but includes version and patch/file details that are unnecessary for deduplication." - }, - "usage": { - "input_tokens": 871, - "output_tokens": 993 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "Two regressions in Discord thread handling: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing progressive context growth and a session resolver fallback to agent:*:main that routes CLI/session commands to the wrong session, leading to increased latency and degraded performance in long threads.", - "solution_summary": "Applied runtime patches to compiled dist bundles (dist/reply-BCcP6j4h.js, dist/compact-1mmJ_KWL.js, dist/pi-embedded-Cz5VjpnY.js, dist/gateway-cli-B-E8XzUM.js, dist/register.agent-jS8B5meo.js), restarted the gateway, created backups (*.bak-rootfix-20260313T023340Z) and an idempotent reapply script (/home/clawd/scripts/openclaw-rootfix-v2026.3.11.sh), and validated thread-bound session resolution and logs.", - "maintainer_signal_summary": "Manually patched compiled runtime bundles listed above; patches are not preserved across reinstalls so maintain the reapply script and backups. Gateway restarted (active) and logs show no new errors. Recommend adding smoke tests for long-thread context size growth and CLI routing correctness.", - "dedupe_summary": "Discord threads: per-turn metadata re-injection causing unbounded context growth and session resolver fallback to agent:*:main (wrong CLI/session routing); patched compiled dist bundles, added backups and a reapply script." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary accurately states the core bug (per-turn metadata re-injection causing unbounded context growth and session resolver fallback to agent:*:main) and the code area patched, though mentioning backups/reapply script adds minor operational noise not needed for dedupe." - }, - "usage": { - "input_tokens": 847, - "output_tokens": 875 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "OpenClaw receives Discord messages and generates responses, but sending the reply fails with \"TypeError: fetch failed\" even though Discord rest/gateway proxy is configured and logged as enabled and the same proxy works with curl.", - "solution_summary": "Ensure the Discord reply send path applies the configured HTTP proxy/agent to fetch calls (or reads proxy env) and add logging/tests to verify the fetch request uses the proxy; likely fix is to attach the proxy agent to REST requests in the reply code path.", - "maintainer_signal_summary": "Regression in OpenClaw 2026.3.2: bot logs show rest/gateway proxy enabled and successful login, messages are received and model responses generated, but reply sending errors with \"TypeError: fetch failed\"; curl through the proxy succeeds, indicating the reply REST requests are not using the configured proxy.", - "dedupe_summary": "Discord reply sending fails with \"TypeError: fetch failed\" despite configured and logged proxy; receiving messages works and curl via proxy succeeds, indicating the reply REST/fetch path isn't using the HTTP proxy." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific description of the core bug (Discord replies failing with fetch/proxy issue and REST path likely bypassing proxy) with no template noise, so it will cluster with similar reports." - }, - "usage": { - "input_tokens": 787, - "output_tokens": 813 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "openclaw v2026.3.11 CLI 'openclaw agent --channel discord --to ...' resolves and delivers to agent:*:main instead of the live session for the specified Discord channel/thread because the session resolver does not lookup by delivery context and Discord targets are not normalized.", - "solution_summary": "Manually patched dist/gateway-cli-BjsM6fWb.js to normalize Discord targets ('148...' → 'channel:148...') and to resolve sessions by delivery context (channel+to+accountId) before falling back to agent:*:main; validated routing to correct session. Note: change is in compiled dist/ and must be ported to source to avoid loss on reinstall.", - "maintainer_signal_summary": "Implement delivery-context-based session lookup and Discord target normalization in source code (not just dist), add CLI routing tests, and replace the ad-hoc compiled-file patch to prevent regression on updates.", - "dedupe_summary": "CLI 'openclaw agent --channel discord --to ...' routes to agent:*:main because session resolver omits lookup by delivery context (channel+to+accountId) and fails to normalize Discord targets ('148...' vs 'channel:148...')." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it highlights the CLI routing to agent:*:main caused by the session resolver not looking up delivery context and failing to normalize Discord targets, which is exactly the core issue for clustering similar session-resolution/routing bugs." - }, - "usage": { - "input_tokens": 852, - "output_tokens": 819 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 a regression in the compiled file dist/compact-1mmJ_KWL.js calls an undefined function resolveAgentMainSessionKey, causing a ReferenceError, agent command failures, and potential gateway crashes.", - "solution_summary": "Manually patched dist/compact-1mmJ_KWL.js by replacing resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...); after the change the ReferenceError is gone and agent/session behavior is restored.", - "maintainer_signal_summary": "This is a high-risk manual edit to compiled dist artifacts and will be lost on update; the correct fix must be applied to the source/build pipeline so the session-resolver patch and this replacement are both present in future builds.", - "dedupe_summary": "ReferenceError: resolveAgentMainSessionKey is not defined in dist/compact-1mmJ_KWL.js — replace with buildAgentMainSessionKey(...) to fix" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the runtime ReferenceError in the compiled file and the exact code change needed, with no extraneous boilerplate, so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 823, - "output_tokens": 640 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "Discord channel plugin shows ON | OK but health check and message send fail with 'fetch failed' when OpenClaw is behind a proxy; channels.discord.proxy is applied to WebSocket but not to REST API calls, while curl with the same proxy succeeds.", - "solution_summary": "Ensure the Discord plugin applies channels.discord.proxy to its HTTP/REST client (health check and message send) in addition to WebSocket connections; update client initialization, add tests, and verify health check uses the proxy.", - "maintainer_signal_summary": "Regression on OpenClaw 2026.3.8: configuring channels.discord.proxy does not route REST API requests through the proxy, causing health check 'fetch failed' and message send timeouts; curl with the proxy works, reproduces by configuring channels.discord.proxy and running openclaw message send.", - "dedupe_summary": "Discord plugin ignores channels.discord.proxy for REST API calls (health check and message send) — only WebSocket uses the proxy, causing 'fetch failed' in proxied environments." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (proxy not applied to REST API calls causing health check and sends to fail) without boilerplate or extraneous details." - }, - "usage": { - "input_tokens": 779, - "output_tokens": 625 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the session resolver in dist/compact-1mmJ_KWL.js for the agent command does not lookup by delivery context (channel + to + accountId), so sessions resolve to agent:*:main instead of the live session for the target channel, causing agent responses to be associated with the wrong session and routing inconsistencies with the CLI gateway.", - "solution_summary": "Manually patched dist/compact-1mmJ_KWL.js to add delivery-context lookup in the agent command session resolver (search by channel + to + accountId) and only fallback to main when no specific session is found; validation shows correct session resolution and response association.", - "maintainer_signal_summary": "High regression risk: this is a manual edit to a compiled file in dist and will be lost on reinstall/update; the fix must be applied in source and rebuilt to be permanent and avoid future regressions.", - "dedupe_summary": "dist/compact-1mmJ_KWL.js session resolver missing delivery-context lookup (channel+to+accountId), causing default to agent:*:main; manual patch added delivery-context lookup but change exists only in compiled dist file." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the affected compiled file, the missing delivery-context lookup (channel+to+accountId), the failure mode (falls back to agent:*:main), and that a manual patch exists only in the dist file." - }, - "usage": { - "input_tokens": 793, - "output_tokens": 603 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English, creating usability barriers for non-English users, particularly Chinese speakers, despite the docs already having zh-CN translations.", - "solution_summary": "Extract all hardcoded UI strings into a structured i18n layer (e.g., JSON locale files), add a zh-CN locale file with translations, provide a language selector or auto-detect, and build infrastructure to add more languages.", - "maintainer_signal_summary": "Strings are currently hardcoded in the control-ui bundle (dist/control-ui/assets/index-*.js); docs/zh-CN exists and can be reused for translations. Recommended scope: extract strings, add zh-CN, add UI language selector/auto-detect, and update build to load locales.", - "dedupe_summary": "Extract hardcoded English strings from OpenClaw Control UI into locale files and add zh-CN translations plus a language selector/auto-detect." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states extracting hardcoded strings to locale files, adding zh-CN translations, and introducing language selection/auto-detect, which accurately captures the i18n feature request for the OpenClaw Control UI." - }, - "usage": { - "input_tokens": 714, - "output_tokens": 475 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "WhatsApp group chats could not apply channel-configured systemPrompt instructions at account or group granularity, preventing per-account or per-group reply customization and wildcard defaults.", - "solution_summary": "Added systemPrompt support to WhatsApp config/schema at channels.whatsapp, accounts., and groups. (including wildcard \"*\"); implemented account resolution, group prompt composition, tests and docs; account-level values override root and account.groups fully replace root groups; final injected prompt is account prompt plus resolved group prompt separated by a blank line.", - "maintainer_signal_summary": "Change scope limited to WhatsApp: no changes to non-WhatsApp channels, hot-reload behavior, or group allowlist/mention-gating semantics; includes clear resolution hierarchy (account prompt > group prompt, specific group > wildcard, account.groups replaces root groups).", - "dedupe_summary": "WhatsApp: add systemPrompt hierarchy — support systemPrompt at channels.whatsapp, accounts., and groups. (with \"*\" wildcard); account-level overrides root and account.groups replace root groups; final prompt = account prompt + resolved group prompt." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the new WhatsApp systemPrompt resolution hierarchy and composition that captures the core change and would match similar PRs/issues." - }, - "usage": { - "input_tokens": 2490, - "output_tokens": 545 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before agent processing, and WhatsApp group policy resolution could ignore account-scoped config (causing allowlist/require-mention checks to fail).", - "solution_summary": "Append processing is enabled only for WhatsApp group messages, and group policy resolution now propagates accountId and uses account-scoped groupAllowFrom/allowFrom fallbacks so account-scoped WhatsApp settings apply; DM append history remains skipped.", - "maintainer_signal_summary": "Change touches WhatsApp inbound and group-activation code (src/web/inbound/monitor.ts, src/web/auto-reply/monitor/group-activation.ts), was validated against a live OpenClaw WhatsApp deployment with trace logs showing before/after behavior, adds targeted tests for account-scoped allowlist fallback, and makes no config/schema/debug/logging or unrelated channel changes.", - "dedupe_summary": "Enable messages.upsert(type=\"append\") processing for WhatsApp groups and use accountId to apply account-scoped groupAllowFrom/allowFrom when resolving WhatsApp group policy defaults." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core change—enabling append processing for WhatsApp groups and using account-scoped accountId for group policy resolution—without extraneous boilerplate." - }, - "usage": { - "input_tokens": 2042, - "output_tokens": 491 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Canvas (A2UI) returned 401 when accessed via Tailscale Serve because the gateway treated requests from the trusted proxy as unauthenticated: resolveClientIp() returned undefined when the proxy sent no forwarding headers, and isLocalDirectRequest() did not fall back to the raw socket address when XFF resolved to a non-loopback Tailscale IP.", - "solution_summary": "Add socket-address fallback in resolveClientIp() when no forwarding headers are present; have isLocalDirectRequest() check req.socket.remoteAddress when allowRealIpFallback is enabled; gate .ts.net host acceptance behind the existing allowTailscale flag and thread auth.allowTailscale through callers; update docs and tests.", - "maintainer_signal_summary": "Changes touch gateway auth/net/server code and tests (src/gateway/{auth.ts,net.ts,server-*.ts}, session-kill-http) and docs; tests added and passing. Behavior change only when no XFF/X-Real-IP headers are present or when allowRealIpFallback is enabled; allowTailscale defaults to true for tailscale.mode = \"serve\" to preserve existing setups. Risk: different callers may now see socket IP when proxies omit forwarding headers; mitigation and docs provided.", - "dedupe_summary": "Canvas returned 401 behind Tailscale Serve because resolveClientIp returned undefined for direct trusted-proxy requests without XFF and isLocalDirectRequest didn’t fall back to socket.remoteAddress; fix adds socket fallback and gates .ts.net host acceptance behind allowTailscale." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that states the root cause (no socket fallback causing 401 behind Tailscale Serve) and the precise fixes (socket.remoteAddress fallback and gating .ts.net via allowTailscale), so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 1881, - "output_tokens": 756 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "checkInboundAccessControl computed an isSelfChat heuristic but never used it as a guard, allowing non-self WhatsApp messages (group messages, DMs from strangers, broadcasts) to pass through because groupPolicy defaulted to \"open\", violating self-chat isolation when selfChatMode was intended to be absolute.", - "solution_summary": "Introduce an early-return guard in checkInboundAccessControl that rejects all non-self messages when account.selfChatMode === true; the guard runs before group/DM policy evaluation and intentionally uses the explicit config flag (not the isSelfChat heuristic) to avoid blocking teammates in mixed allowFrom lists.", - "maintainer_signal_summary": "Guard is placed before group and DM policy resolution and preserves isSelfChat in the result for UI/logging; auto-detected self-chat via heuristic is not hard-blocked by design; tests pass and the change can be reverted by removing the early-return guard.", - "dedupe_summary": "Explicit selfChatMode true did not block non-self WhatsApp messages because isSelfChat was computed but unused and groupPolicy defaulted to \"open\"; fix adds an early-return that rejects non-self messages when account.selfChatMode === true." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the bug (isSelfChat computed but unused, groupPolicy defaulting to open) and the exact fix (early-return rejecting non-self messages when account.selfChatMode === true), so it will cluster correctly with similar changes." - }, - "usage": { - "input_tokens": 1596, - "output_tokens": 735 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "With groupPolicy: \"allowlist\", group messages from senders not in groupAllowFrom are dropped at the access-control layer (allowed: false → continue) and never stored for context, so the agent cannot see those messages in its \"[Chat messages since your last reply]\" context block and documented mention-gating behavior is not met.", - "solution_summary": "Introduce an opt-in groupContextFromAll flag (top-level and per-account, per-account wins) defaulting to false; when enabled, access-control returns storeForContext for non-allowlisted group senders, inbound monitor marks them as contextOnly (skips read receipts) and records them in group history as pending context without triggering replies; added types, schema, and account config changes while preserving backward compatibility.", - "maintainer_signal_summary": "Changes touch src/web/inbound/access-control.ts, monitor.ts, types.ts, auto-reply/on-message.ts, config types and zod schema, and accounts resolution. Potential risks: allowlist normalization/matching may misclassify senders, and the new context-only early path bypasses some existing group gating/state updates, which could cause subtle inconsistencies.", - "dedupe_summary": "Add groupContextFromAll option to store non-allowlisted WhatsApp group messages as pending context (contextOnly) instead of dropping them when groupPolicy is allowlist." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core change—new groupContextFromAll option to store non-allowlisted WhatsApp group messages as pending context instead of dropping them—without boilerplate." - }, - "usage": { - "input_tokens": 1613, - "output_tokens": 799 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "sessions_spawn always created a new random-UUID session for each sub-agent spawn, preventing reuse of conversation history and enabling workarounds that bypass allowlists.", - "solution_summary": "Add an optional sessionKey parameter so sub-agents can run in a deterministic session. Short keys map to agent:{agentId}:subagent:{key}; fully-qualified keys containing \":subagent:\" are validated to match the targetAgentId to prevent cross-agent session injection; omission falls back to crypto.randomUUID(). Includes four tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch rejection.", - "maintainer_signal_summary": "Blocking test import: a test file imports a non-existent createOpenClawTools from ./openclaw-tools.js instead of the correct ./clawdbot-tools.js, causing compile failure. Case-sensitivity bug: embedded agentId extracted from fully-qualified keys is not normalized/lowercased while targetAgentId is normalized, causing false mismatch rejections. Inconsistent error handling: cross-agent mismatch uses throw new Error(...) while other security checks return jsonResult(...), producing different error surfaces.", - "dedupe_summary": "sessions_spawn now accepts an optional sessionKey to reuse deterministic sub-agent sessions; short keys map to agent:{agentId}:subagent:{key}, fully-qualified keys are validated to match targetAgentId to prevent cross-agent session injection, otherwise falls back to crypto.randomUUID()." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change (optional sessionKey for deterministic sub-agent sessions), mapping rules, validation to prevent cross-agent injection, and fallback behavior without template noise." - }, - "usage": { - "input_tokens": 1666, - "output_tokens": 711 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) via the generic edit path calls editMessageText and fails with 400: \"there is no text in the message to edit\", causing message edits to silently error and leave the UI inconsistent.", - "solution_summary": "editMessageTelegram now detects that error via a NO_TEXT_IN_MESSAGE_RE regex, retries the edit with editMessageCaption for captionable media, routes buttons-only updates to editMessageReplyMarkup, preserves HTML parse fallback, and treats MESSAGE_NOT_MODIFIED as success; no public API or config changes.", - "maintainer_signal_summary": "Changes live in src/telegram/send.ts: added NO_TEXT_IN_MESSAGE_RE and retry logic from editMessageText → editMessageCaption, plus direct editMessageReplyMarkup path for buttons-only updates; six unit tests added covering retry, buttons-only, error propagation, and MESSAGE_NOT_MODIFIED handling; backward compatible and only affects the Telegram adapter; risk: regex may need updates if Telegram changes error wording.", - "dedupe_summary": "Telegram editMessageText returns 400 'there is no text in the message to edit' for media messages; fix retries with editMessageCaption and uses editMessageReplyMarkup for buttons-only updates." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that states the specific Telegram API error and the implemented retry/fallback (editMessageCaption and editMessageReplyMarkup), with no boilerplate or extraneous details." - }, - "usage": { - "input_tokens": 1547, - "output_tokens": 711 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "PR #24962 hardcoded disableBlockStreaming:true for WhatsApp to address a reasoning-content leak, which unintentionally disabled block streaming entirely (messages were batched and sent at the end) and ignored per-account and global block streaming config.", - "solution_summary": "Changed the deliver callback to only suppress tool payloads and made disableBlockStreaming driven by per-account channels.whatsapp.blockStreaming with a fallback to agents.defaults.blockStreamingDefault; upstream reasoning suppression remains in place.", - "maintainer_signal_summary": "Reasoning/thinking content is still filtered upstream by shouldSuppressReasoningPayload; tool payloads remain suppressed; tests (including live WhatsApp verification and inbound contract tests) pass; default behavior unchanged unless explicitly configured.", - "dedupe_summary": "WhatsApp block streaming was hardcoded off (disableBlockStreaming:true), ignoring channels.whatsapp.blockStreaming and agents.defaults.blockStreamingDefault; this change restores config-driven block streaming and limits deliver suppression to tool payloads while keeping reasoning suppression upstream." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core bug (hardcoded disableBlockStreaming for WhatsApp), the config-driven restoration, and the narrowed deliver suppression to tool payloads while noting reasoning filtering remains upstream." - }, - "usage": { - "input_tokens": 1539, - "output_tokens": 634 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Sending videos larger than the previous 5MB default caused the Telegram gateway to hang: fetchRemoteMedia hit a content-length > maxBytes and closeDispatcher (undici Agent.close) deadlocked when the response body was unconsumed, leaving the gateway unresponsive and previously dropping messages when media failed.", - "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher to timeout and fall back to destroy to avoid Agent.close deadlocks, added timeouts and retry logic to getFile and downloads, made media download errors non-fatal so text/captions are forwarded, and extended UI metadata stripping for inbound media paths.", - "maintainer_signal_summary": "Changes touch src/infra/net/ssrf.ts and src/telegram/bot.ts; default mediaMaxMb is now 20 but remains configurable via channels.telegram.mediaMaxMb; no SSRF policy, media storage, or agent-side processing changes; strip-inbound-meta tests updated and pass.", - "dedupe_summary": "Telegram gateway hung on video messages >5MB due to undici Agent.close deadlock when response body was unconsumed; fix increases channels.telegram.mediaMaxMb to 20, adds timeouts/retries for media download, makes media errors non-fatal, and forces Agent.close to timeout then destroy." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and technical summary that states the root cause (undici Agent.close deadlock) and the specific fixes (increase mediaMaxMb, timeouts/retries, non-fatal media errors, forced Agent.close destruction), with no boilerplate — ideal for deduplication." - }, - "usage": { - "input_tokens": 1635, - "output_tokens": 897 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "Telegram document attachments sent without caption were always represented as \"\" in the agent-facing inbound body, mislabeling documents (PDF/DOCX/etc.) as images and causing agents to respond as if no document was received.", - "solution_summary": "Changed resolveTelegramInboundBody to use the precomputed type-aware placeholder (e.g. \"\") instead of a hardcoded \"\" for non-audio media; added unit tests covering placeholders for all media types and a test verifying document messages trigger getFile.", - "maintainer_signal_summary": "Scope limited to the agent-facing fallback body text in Telegram integration; media download, understanding pipeline, and file extraction unchanged. Tests added in extensions/telegram/src/bot/helpers.test.ts and extensions/telegram/src/bot.create-telegram-bot.test.ts. Risk: downstream code that pattern-matches only \"\" might be affected, but this was an edge-case fallback and the change is more correct.", - "dedupe_summary": "Telegram inbound-body fallback used hardcoded \"\" for non-audio media, causing documents to be labeled as images; fixed to use the type-aware placeholder so document attachments produce \"\"." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the bug (hardcoded placeholder) and the fix (use type-aware placeholder producing ), with relevant technical terms and no boilerplate." - }, - "usage": { - "input_tokens": 1462, - "output_tokens": 637 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "MEDIA_MAX_BYTES was hardcoded to 5 MB in src/media/store.ts, causing files larger than 5 MB to fail to stage into sandboxed agents and making common file types unusable in sandboxed workflows.", - "solution_summary": "Introduced a new media.maxBytes config field, added getMediaMaxBytes()/setMediaMaxBytes() accessors and wired gateway boot to read and apply the config, updated error messages to show the configured limit; default remains 5 MB and schema caps values at 500 MB.", - "maintainer_signal_summary": "All relevant tests pass (56 tests across four files). Backwards compatible: MEDIA_MAX_BYTES exported (deprecated) and default preserved. Schema enforces 500 MB max; fixed previous issues with unset config and MB formatting; risk of disk fill noted and mitigated by TTL and schema cap.", - "dedupe_summary": "Make MEDIA_MAX_BYTES configurable via media.maxBytes; replace hardcoded 5MB staging limit with a runtime-configurable limit (default 5MB, maximum 500MB) so sandboxed agents can accept larger files." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Clear, concise technical summary that captures the core change (replace hardcoded 5MB staging limit with media.maxBytes config and default/maximum values) without template noise, so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 1583, - "output_tokens": 476 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "When Telegram media download fails due to exceeding size limits or a Telegram API error, the handler returned early and the entire message (including caption/text) was silently dropped; the agent received no ack and the user got no indication their file was lost.", - "solution_summary": "On download failure, extract file metadata (name, size, type) from the Telegram message object and inject a metadata stub into msg.text instead of returning early, allowing the message to continue through the normal pipeline so the agent sees the file and reason for failure.", - "maintainer_signal_summary": "Change implemented in src/telegram/bot-handlers.ts: buildFileMetadataStub populates msg.text on error paths for files over mediaMaxMb or Telegram's 20MB getFile limit; normal media download, media groups, sticker handling, and outbound sending are unchanged. No new network calls or permissions; risk is mutating msg.text only on previously-dropped messages and can be reverted by reverting the single commit to bot-handlers.ts.", - "dedupe_summary": "Telegram media download failures (file too large or API error) previously caused the message to be dropped; now file metadata stub is injected into msg.text so the agent receives the file and failure reason." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: states the bug (Telegram media download failures dropped messages) and the exact fix (inject file metadata stub into msg.text so the agent sees the file and failure reason), which will match similar issues." - }, - "usage": { - "input_tokens": 1425, - "output_tokens": 634 - } - }, - { - "thread_id": 5796, - "number": 24117, - "kind": "pull_request", - "title": "feat(cli): add --session-key targeting for openclaw agent", - "summary": { - "problem_summary": "The openclaw agent CLI lacked a stable targeting option, causing CLI-triggered deliveries and callback automation to resolve to the agent main session because sessionId is UUID-based and --to or --agent are unreliable in some environments.", - "solution_summary": "Added a new --session-key CLI flag, extended AgentCliOpts with sessionKey, forwarded it into resolveSessionKeyForRequest and the gateway call, updated help/validation text, and added tests to verify parsing and forwarding. Session resolution precedence is now session-key > session-id > to > agent.", - "maintainer_signal_summary": "Safe, backward-compatible change touching src/cli/program/register.agent.ts and src/commands/agent-via-gateway.ts, adds tests for CLI parsing and gateway parameter forwarding, minimal risk and straightforward to review and merge.", - "dedupe_summary": "Add --session-key flag to openclaw agent CLI; forward sessionKey through AgentCliOpts into resolveSessionKeyForRequest so session targeting uses session-key over session-id/to/agent." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core change—adding a --session-key CLI flag and plumbing it through AgentCliOpts to resolveSessionKeyForRequest to prioritize session-key targeting—without extraneous boilerplate." - }, - "usage": { - "input_tokens": 1397, - "output_tokens": 531 - } - }, - { - "thread_id": 766, - "number": 11611, - "kind": "pull_request", - "title": "feat: separate group-level allowlist from sender-level command authorization", - "summary": { - "problem_summary": "With groupPolicy set to \"allowlist\", the system currently gates all group interactions (chat and slash commands) by sender phone number via groupAllowFrom, preventing: allowing specific groups while letting anyone chat, restricting slash commands to owners only, and blocking spam groups while keeping approved groups open.", - "solution_summary": "When groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access is gated by the groups JID allowlist (only listed groups accepted) while anyone in an approved group may chat; groupAllowFrom continues to control slash-command authorization. If no groups mapping is present, behavior falls back to the legacy sender-based filtering.", - "maintainer_signal_summary": "Changes localized to src/web/inbound/access-control.ts (use resolveChannelGroupPolicy), updated JSDoc in src/config/types.whatsapp.ts, and four new Vitest tests for group allowlist behavior; depends on PR #3326 to require explicit groupPolicy: \"allowlist\" when using groups config and includes a minor doc/comment update.", - "dedupe_summary": "WhatsApp: groupPolicy \"allowlist\" now uses channels.whatsapp.groups JID mapping as the group-level allowlist (allows any participant to chat in approved groups) while groupAllowFrom still limits slash commands; falls back to sender-based filtering when no groups mapping is configured." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it captures the core change to use the channels.whatsapp.groups JID mapping as the group-level allowlist, the resulting open-chat behavior, that groupAllowFrom still gates slash commands, and the sender-filtering fallback." - }, - "usage": { - "input_tokens": 1447, - "output_tokens": 558 - } - }, - { - "thread_id": 17924, - "number": 46425, - "kind": "pull_request", - "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", - "summary": { - "problem_summary": "Telegram's configured mediaMaxMb limit was ignored for outbound reply messages because loadWebMedia in the reply delivery path received no maxBytes and fell back to the default 100 MB, so smaller operator limits had no effect.", - "solution_summary": "Propagated mediaMaxBytes through the Telegram outbound reply delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia receives the configured maxBytes; added unit tests and kept behavior unchanged when unset.", - "maintainer_signal_summary": "Direct-send path in send.ts was already correct; this change only threads mediaMaxBytes through reply and native-commands delivery paths, is backward compatible (mediaMaxBytes optional), and unit tests covering the change pass.", - "dedupe_summary": "Telegram outbound reply delivery ignored telegramAccounts.mediaMaxMb (loadWebMedia got no maxBytes and defaulted to 100MB); mediaMaxBytes is now threaded through bot.ts, bot-message.ts, bot-message-dispatch.ts, delivery.replies.ts and bot-native-commands.ts so replies enforce the configured limit." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the bug (reply delivery ignored mediaMaxMb) and the fix (threaded mediaMaxBytes through the listed Telegram outbound delivery files) with no template noise, so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 1448, - "output_tokens": 732 - } - }, - { - "thread_id": 5712, - "number": 37050, - "kind": "pull_request", - "title": "fix(telegram): add HTML parse fallback for media captions", - "summary": { - "problem_summary": "Telegram media send functions (photo, document, video, animation, audio, voice, video note) lacked the HTML-parse fallback used by text messages, so captions with invalid HTML/entities caused the whole media send to fail with a 400 \"can't parse entities\" error.", - "solution_summary": "Wrapped the media-send callback in withTelegramHtmlParseFallback so on an HTML parse error the send is retried without parse_mode and with the raw caption; this applies to all media types and a unit test verifies the initial failure and subsequent retry behavior. No changes were made to text message, edit message, thread fallback, or caption-splitting logic.", - "maintainer_signal_summary": "Change made in src/telegram/send.ts; added unit test 'retries media caption as plain text on HTML parse error' and all tests pass. Backward-compatible; revert by removing the fallback wrapper; risk is only that fallback sends raw text instead of rendered HTML.", - "dedupe_summary": "Telegram media sends (sendPhoto/sendDocument/sendVideo/sendAnimation/sendAudio/sendVoice/video note) failed with 400 \"can't parse entities\" for captions containing invalid HTML; now retries by removing parse_mode and sending caption as plain text." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core bug and fix naming affected APIs and error and omitting boilerplate, so it will cluster correctly with similar reports." - }, - "usage": { - "input_tokens": 1322, - "output_tokens": 703 - } - }, - { - "thread_id": 21769, - "number": 55280, - "kind": "pull_request", - "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", - "summary": { - "problem_summary": "WhatsApp had block streaming effectively disabled and a delivery race in the block-reply pipeline that could suppress both intermediate blocks and the final reply. disableBlockStreaming was hardcoded and its assignment used undefined which prevented activation, and onBlockReply used a sync enqueue so the pipeline marked sentContentKeys before actual delivery.", - "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming; fix disableBlockStreaming assignment; add blockStreamingCoalesceDefaults; implement dispatcher.sendBlockReplyAsync and await it in onBlockReply so sentContentKeys are only marked after confirmed delivery; update tests for streaming-on and streaming-off paths.", - "maintainer_signal_summary": "Key changes: process-message.ts now resolves account blockStreaming and fixes disableBlockStreaming assignment; shared.ts adds coalesce defaults; reply-dispatcher.ts adds sendBlockReplyAsync with per-item settlement; dispatch-from-config.ts awaits sendBlockReplyAsync; tests updated and new cases added to cover streaming enabled/disabled. This fixes the root cause of silent turns by ensuring block delivery is confirmed before marking sent.", - "dedupe_summary": "WhatsApp block streaming race: onBlockReply used sync sendBlockReply causing pipeline to mark sentContentKeys before confirmed delivery and suppress final replies; fixed by making blockStreaming configurable, correcting disableBlockStreaming handling, and adding sendBlockReplyAsync with awaiting of confirmed delivery." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that identifies the core race condition in WhatsApp block-reply handling, the incorrect disableBlockStreaming handling, and the implemented fix (awaiting delivery via sendBlockReplyAsync), so it would cluster with similar issues." - }, - "usage": { - "input_tokens": 1235, - "output_tokens": 845 - } - }, - { - "thread_id": 8098, - "number": 25543, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked a group-level allowlist option; groups could not be explicitly allowed by ID and thus were still subject to sender-level groupAllowFrom checks and could not run control commands.", - "solution_summary": "Added channels.signal.groups. config and SignalGroupConfig type, extended Zod schema, wired resolveChannelGroupPolicy into the Signal event handler to allow group-level bypass for message delivery and command authorization, and updated docs with examples.", - "maintainer_signal_summary": "Implementation follows existing patterns used by other channels, preserves backward compatibility via hasGroupAllowFrom, TypeScript compilation passes, and the change was manually tested; safe to merge.", - "dedupe_summary": "Add channels.signal.groups. allowlist to bypass sender-level groupAllowFrom and permit control commands for explicitly allowed Signal groups." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary describing the new Signal groups allowlist that bypasses sender-level checks and enables command authorization for allowed groups, which is exactly the core change for deduplication." - }, - "usage": { - "input_tokens": 1177, - "output_tokens": 491 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal was the only channel missing group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter and group IDs never matched sender IDs.", - "solution_summary": "Added a groups config to SignalAccountSchemaBase and updated Signal's event handler to call resolveChannelGroupPolicy before falling back to sender-level groupAllowFrom; added four tests and verified existing tests pass.", - "maintainer_signal_summary": "Signal lacked the groups config and the resolveChannelGroupPolicy access check used by other channels; fix adds the groups schema and invokes resolveChannelGroupPolicy in Signal's group access path so group IDs in channels.signal.groups. permit group messages.", - "dedupe_summary": "Signal lacked group-level allowlist support (channels.signal.groups.); groupAllowFrom is sender-level so group IDs never matched; added groups config and resolveChannelGroupPolicy call to allow group-level allowlisting." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the bug (missing group-level allowlist for Signal due to misuse of sender-level groupAllowFrom) and the fix (added groups config and resolveChannelGroupPolicy), so it will match similar issues precisely." - }, - "usage": { - "input_tokens": 977, - "output_tokens": 504 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "WhatsApp messages are being batched because process-message.ts hardcodes disableBlockStreaming: true, causing intermediate agent blocks to accumulate and be sent as one final payload.", - "solution_summary": "Replace the hardcoded disableBlockStreaming with a config-driven resolveWhatsAppBlockStreamingEnabled helper, register blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in the WhatsApp dock, and fix queuedFinal logic so intermediate block payloads count as delivered; tests updated to cover enabled/disabled behavior.", - "maintainer_signal_summary": "When channels.whatsapp.blockStreaming is true, intermediate blocks are delivered as separate WhatsApp messages immediately; when false or omitted, behavior remains backward-compatible with a single final reply. Change aligns WhatsApp behavior with Telegram/Line.", - "dedupe_summary": "Respect channels.whatsapp.blockStreaming instead of hardcoded disableBlockStreaming:true so intermediate blocks are delivered as separate WhatsApp messages; fix queuedFinal marking and register the capability/default." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core change—making WhatsApp block streaming config-driven, fixing queuedFinal delivery behavior, and registering the capability—without template noise." - }, - "usage": { - "input_tokens": 727, - "output_tokens": 692 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v4-terse.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v4-terse.json deleted file mode 100644 index 49df43c..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v4-terse.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v4-terse", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "Extract the core topic from this GitHub issue or PR for duplicate clustering. Ignore all template boilerplate, testing notes, checklists, environment details, and reproduction steps. Focus only on WHAT the issue/change is about technically.\n\nReturn JSON: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary. Plain text, 1-2 sentences each. Be extremely concise — every word in dedupe_summary must add signal for finding duplicates.", - "timestamp": "2026-03-30T17:05:31.524Z", - "aggregate": { - "avg_score": 4.47, - "avg_signal_density": 4.5, - "boilerplate_count": 1, - "boilerplate_pct": 3, - "would_cluster_correctly_pct": 100, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 41983, - "total_output_tokens": 27511 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "channels.discord.proxy only proxies the Discord WebSocket gateway; startup REST API calls (deploy commands, fetch bot identity) and guild resolve do not use the proxy, causing REST timeouts and guild channels to be non-functional behind an HTTP proxy.", - "solution_summary": "Ensure the configured proxy is applied to the undici dispatcher used for all Discord REST calls and guild resolution during provider startup (or add a global proxy setting), e.g., set a ProxyAgent as the global dispatcher and add retry logic for guild resolve.", - "maintainer_signal_summary": "Regression: WebSocket connects via proxy but REST calls time out with 'fetch failed' on startup; Node's undici fetch ignores HTTP_PROXY; adding a ProxyAgent fixes REST but guild resolve still uses an unproxied code path; reproduces on OpenClaw 2026.3.13 with Node 25.", - "dedupe_summary": "discord proxy websocket-only rest-api guild-resolve fails undici fetch no-proxy ProxyAgent" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise set of technical keywords that capture the core issue (proxying only WebSocket, REST API/guild resolve failures, undici/ProxyAgent), but it's fragmented and could be a clearer single-sentence summary." - }, - "usage": { - "input_tokens": 2185, - "output_tokens": 749 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "Discord outbound REST calls (sending messages/attachments) in OpenClaw ignore channels.discord.proxy because the RequestClient fetch doesn't use the configured proxy, causing 'fetch failed' errors in proxy-restricted environments.", - "solution_summary": "Update RequestClient/fetch to honor channels.discord.proxy and add proxy support (HTTP/SOCKS/tunneling) for REST API and attachment transfers so all Discord traffic routes through the configured proxy.", - "maintainer_signal_summary": "User set channels.discord.proxy=http://127.0.0.1:7890/; WebSocket gateway receives messages (proxy working) but REST sends fail with 'fetch failed'; logs show rest proxy enabled but REST not using proxy; OpenClaw 2026.2.26 on restricted-region network.", - "dedupe_summary": "Discord REST ignores channels.discord.proxy; RequestClient fetch lacks proxy support." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: states that Discord REST calls ignore channels.discord.proxy because the underlying RequestClient fetch has no proxy support, which clearly captures the core bug for deduplication." - }, - "usage": { - "input_tokens": 867, - "output_tokens": 623 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "When Discord is configured behind an HTTP proxy, OpenClaw successfully logs in (gateway/WebSocket) and fetches applicationId but some startup REST calls—native command deployment (/applications/.../commands) and bot identity fetch—fail with 'fetch failed'.", - "solution_summary": "Unify proxy handling by routing all Discord REST requests through the same proxy-aware HTTP client/dispatcher used by the gateway: propagate channels.discord.proxy to deploy/identity request code paths or replace divergent fetch implementations, and add tests/logging to verify proxy usage.", - "maintainer_signal_summary": "Startup logs show 'rest proxy enabled' and 'gateway proxy enabled', applicationId fetch succeeded, but deploy-rest PUT and fetch-bot-identity both error with 'fetch failed' while the provider still reports 'logged in to discord', indicating inconsistent proxy application across transport paths.", - "dedupe_summary": "discord proxy fetch-failed native-command-deploy bot-identity-fetch inconsistent-proxy REST-vs-gateway /applications/.../commands channels.discord.proxy logged-in" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused keywords that capture the core issue: Discord REST calls (native command deploy and bot identity fetch) failing behind channels.discord.proxy while the gateway/login works, with no template noise." - }, - "usage": { - "input_tokens": 1013, - "output_tokens": 754 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "OpenClaw logs the Discord channel proxy as enabled but discord.js REST requests fail with 'fetch failed' and the WebSocket gateway never establishes under WSL2, despite the proxy working for curl.", - "solution_summary": "Ensure OpenClaw actually applies the configured HTTP proxy to the Discord.js REST client (or sets HTTP(S)_PROXY / an appropriate agent/undici proxy option) so fetch and gateway connections use the proxy.", - "maintainer_signal_summary": "Logs show 'rest proxy enabled' and 'gateway proxy enabled' yet REST calls error 'fetch failed' and gatewayConnected=false; reproducible on WSL2 with proxy 127.0.0.1:10808 while curl succeeds and Feishu channel works.", - "dedupe_summary": "openclaw discord proxy-not-applied fetch-failed discord.js WSL2 gatewayConnected=false rest-api" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Captures the core issue (Discord proxy not being applied causing REST fetch failures under WSL2 with discord.js) but is terse and contains a noisy token ('gatewayConnected=false') that isn't necessary for deduplication." - }, - "usage": { - "input_tokens": 850, - "output_tokens": 777 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "Discord REST API HTTP requests ignore configured proxy (channels.discord.proxy and HTTP_PROXY), causing 'TypeError: fetch failed' when sending messages while WebSocket and other channels work.", - "solution_summary": "Route Discord REST API calls through the existing proxy-aware fetch/SSRF guard used for web_search/web_fetch so they honor channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY.", - "maintainer_signal_summary": "Repro on OpenClaw 2026.2.26 (Node v25.5.0, macOS); WebSocket receives events, curl via proxy to Discord API succeeds, but bot reply REST calls fail with 'TypeError: fetch failed'; changelog notes web tools proxy fix excluded Discord REST.", - "dedupe_summary": "discord REST API ignores proxy; channels.discord.proxy/HTTP_PROXY not applied; fetch failed on send message; WebSocket ok; web_search proxy fix missing for discord" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary concisely captures that Discord REST calls ignore the configured proxy and fail with fetch errors while WebSocket works, but adds a possibly incorrect/unclear mention of a 'web_search' proxy fix which is minor noise." - }, - "usage": { - "input_tokens": 873, - "output_tokens": 516 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "The CLI command openclaw agent --channel discord --to delivers messages to agent:*:main instead of the live Discord channel/thread because the session resolver does not search by delivery context and Discord targets are not normalized, and the command path fails to propagate channel/account context.", - "solution_summary": "Manually patched compiled dist files to add session lookup by delivery context (channel+to+accountId), normalize Discord targets, pass channel and replyAccount/accountId through the command path, and use buildAgentMainSessionKey for correct lookup.", - "maintainer_signal_summary": "High-risk manual fixes applied to dist/gateway-cli-*.js, dist/compact-*.js, and dist/register.agent-*.js; change must be implemented in source, tested, and preserved across updates (add revalidation script/tests).", - "dedupe_summary": "CLI agent misroutes to agent:*:main; session resolver missing delivery-context lookup (channel+to+accountId); Discord target normalization bug; patched dist/*.js" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and focused: identifies CLI misrouting, missing delivery-context lookup (channel+to+accountId), Discord target normalization bug, and patched dist files—no template noise and uses specific technical terms for correct clustering." - }, - "usage": { - "input_tokens": 800, - "output_tokens": 699 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions: Discord-thread messages had volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) re-injected every turn causing context bloat/degradation; CLI agent commands routed to the default main session instead of the live thread-bound session.", - "solution_summary": "Prevent repeated injection of Discord-thread volatile metadata and update CLI/gateway session resolution to look up sessions by actual delivery context (normalized channel + to + accountId), passing channel and replyAccount/accountId and fixing resolveAgentMainSessionKey misuse; changes applied to compiled dist bundles.", - "maintainer_signal_summary": "Manual patches applied to dist/*.js (pi-embedded, gateway-cli, compact, register.agent) and openclaw-gateway.service restarted; verification shows threads no longer repeat metadata and CLI routes correctly; note patches in openclaw/dist/ will be lost on update—reapplication script required.", - "dedupe_summary": "discord threads volatile metadata re-injection repeated metadata per-turn message_id reply_to_id timestamp \"Thread starter\" \"Chat history\" CLI routing wrong session session resolver channel to accountId compiled dist bundles manual patch" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary that captures both core regressions—per-turn re-injection of volatile Discord thread metadata and incorrect CLI session routing/resolver behavior—using specific technical terms suitable for deduplication." - }, - "usage": { - "input_tokens": 719, - "output_tokens": 577 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "The sandbox media staging cap is hardcoded as const MEDIA_MAX_BYTES = 5 * 1024 * 1024 in dist/store-*.js, so files larger than 5MB are rejected and cannot be overridden via openclaw.json.", - "solution_summary": "Make MEDIA_MAX_BYTES configurable (e.g., read tools.media.maxBytes from openclaw.json or honor per-type limits) with a sensible default, and update distributed build files to use the configured value.", - "maintainer_signal_summary": "Regression introduced before v2026.3.7 and present in v2026.3.8; occurs in distributed dist/store-*.js for npm global installs; workaround is manual sed patch of dist files and gateway restart; reproduces when sandbox.mode is non-main.", - "dedupe_summary": "MEDIA_MAX_BYTES hardcoded 5MB sandbox staging limit not configurable tools.media.maxBytes openclaw.json dist/store-*.js regression v2026.3.7 v2026.3.8 npm-global" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "The summary concisely identifies the core issue — a hardcoded 5MB MEDIA_MAX_BYTES sandbox staging limit and relevant config/file locations — but includes noisy version tags and an unrelated 'npm-global' token that aren't helpful for deduplication." - }, - "usage": { - "input_tokens": 766, - "output_tokens": 691 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "OpenClaw's Discord REST API requests fail with 'fetch failed' even though rest proxy is configured and enabled; WebSocket gateway and curl via the same proxy work fine.", - "solution_summary": "Ensure the REST client honors the configured proxy—pass a proxy agent to the Node fetch/undici client or honor HTTP(S)_PROXY env vars and implement HTTPS CONNECT proxy handling for Discord API requests.", - "maintainer_signal_summary": "Logs report 'rest proxy enabled' but REST operations (deploy commands, fetch bot identity, send messages) error with 'fetch failed' while gateway/WebSocket and Telegram work and curl with the same proxy succeeds, indicating proxy not applied to REST fetch.", - "dedupe_summary": "discord REST fetch failed proxy-enabled OpenClaw undici fetch not-using-proxy https-connect" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific keywords capture the core problem (Discord REST requests failing despite proxy configuration, implicating OpenClaw/undici proxy usage and https-connect) with no boilerplate or noise." - }, - "usage": { - "input_tokens": 711, - "output_tokens": 820 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "Discord REST API requests (via @buape/carbon's RequestClient) ignore openclaw.json channels.discord.proxy and call globalThis.fetch without a proxy-aware dispatcher, causing REST calls, attachments, and memorySearch to bypass the configured proxy while the WebSocket gateway respects it.", - "solution_summary": "Update @buape/carbon's RequestClient to accept an optional dispatcher/proxy option (or otherwise use an undici proxy-aware dispatcher) and have OpenClaw pass channels.discord.proxy so REST requests, attachments, and memorySearch use the configured proxy.", - "maintainer_signal_summary": "Repro on Node.js 22; WebSocket gateway already uses proxy; REST client uses native fetch and ignores HTTP_PROXY/HTTPS_PROXY unless --use-env-proxy (which breaks WebSocket). Workaround is setting undici EnvHttpProxyAgent via preload to setGlobalDispatcher; recommended fix is plumbing proxy into RequestClient.", - "dedupe_summary": "Discord REST (@buape/carbon RequestClient) ignores channels.discord.proxy; uses globalThis.fetch without undici dispatcher on Node.js 22; attachments/memorySearch bypass proxy." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core bug (RequestClient/REST calls ignoring channels.discord.proxy and using globalThis.fetch/undici-less path, plus attachments/memorySearch bypassing proxy) but includes a Node.js version mention that is unnecessary noise." - }, - "usage": { - "input_tokens": 720, - "output_tokens": 808 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "Under sustained load OpenClaw experiences rising per-turn latency and serialization due to unbounded context token growth (writer ~42k, reasoning ~90k), long-running turns blocking Discord lanes, and typing indicators remaining until TTL.", - "solution_summary": "Applied configuration limits: reduce concurrency (acp.maxConcurrent 12→6, agents.maxConcurrent 3→2, subagents 8→4), set agents.defaults.contextTokens=80000, increase channels.discord.eventQueue.listenerTimeout 120000→300000, and improved patching script coverage.", - "maintainer_signal_summary": "Gateway restarted to active (running) and no new \"lane wait exceeded\" or \"typing TTL reached\" errors observed; patch script at openclaw-rootfix-v2026.3.11.sh:73; recommendation to expose adaptive/tunable token and concurrency limits.", - "dedupe_summary": "context-token-growth, long-turn lane-blocking, discord lane serialization, typing-TTL-stuck, concurrency-saturation, eventQueue-listenerTimeout, contextTokens=80000, reduced-maxConcurrent" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise, technical keywords capture the core issues (context token growth, lane blocking/serialization, typing TTL, concurrency, event queue timeout) but is a bit fragmentary and includes a specific token count/config tweak that is unnecessary noise." - }, - "usage": { - "input_tokens": 654, - "output_tokens": 766 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "Discord threads in v2026.3.11 repeatedly re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, causing context contamination, token growth, and prompt-cache invalidation.", - "solution_summary": "Manually patched compiled dist files to stop per-turn metadata injection and added an isNewSession guard (dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js), and replaced resolveAgentMainSessionKey with buildAgentMainSessionKey; recommend applying the fix in source and adding post-update validation.", - "maintainer_signal_summary": "HIGH regression risk: fixes made to compiled/dist files will be lost on update; affected artifacts: dist/pi-embedded-D6PpOsxP.js (metadata injection) and dist/compact-1mmJ_KWL.js (session handler/isNewSession, session-key change). Upstream source fix and test needed to prevent prompt-cache bust in long Discord threads.", - "dedupe_summary": "Discord thread metadata re-injected every turn; message_id reply_to_id timestamp \"Thread starter\" \"Chat history\" prompt-cache-bust dist/pi-embedded-D6PpOsxP.js dist/compact-1mmJ_KWL.js isNewSession compiled-dist patch" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core bug (Discord thread volatile metadata re-injected each turn), lists affected fields and compiled files, and contains no template boilerplate." - }, - "usage": { - "input_tokens": 666, - "output_tokens": 783 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "In v2026.3.11 Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, token bloat and prompt cache busting.", - "solution_summary": "Applied manual patch to dist/pi-embedded-D6PpOsxP.js to stop reinjecting that metadata after the first turn; recommend committing a source-level fix and a post-update revalidation script to avoid regressions.", - "maintainer_signal_summary": "Issue is in compiled artifact dist/pi-embedded-D6PpOsxP.js in OpenClaw v2026.3.11; high regression risk because changes were made to dist—fix must be propagated to source and preserved across updates.", - "dedupe_summary": "Discord thread metadata reinjected every turn, message_id reply_to_id timestamp 'Thread starter' 'Chat history', prompt cache bust, dist/pi-embedded-D6PpOsxP.js, v2026.3.11" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise and specific about the core bug (Discord thread metadata being re‑injected each turn, fields affected, cache/token impact, and offending compiled file); minor noise is the explicit version tag which isn't needed for deduping." - }, - "usage": { - "input_tokens": 674, - "output_tokens": 721 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "Two runtime regressions in Discord threads: per-turn re-injection of message metadata causing unbounded context growth, and the session resolver falling back to a generic agent:*:main so CLI commands route to the wrong session.", - "solution_summary": "Applied hotfixes to compiled runtime bundles to stop redundant metadata injection and to make resolver lookup use delivery context (channel+to+accountId); added idempotent re-application script and backups, then restarted and validated the gateway.", - "maintainer_signal_summary": "Fixes are manual edits to compiled dist/*.js files (listed in the PR), backed up (*.bak-rootfix-20260313T023340Z) and preserved by an idempotent reapply script (/home/clawd/scripts/openclaw-rootfix-v2026.3.11.sh); these changes will be lost on updates/reinstall so reapplication must be maintained and non-regression tests added.", - "dedupe_summary": "Discord threads; per-turn metadata duplication (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing context growth; session resolver fallback to agent:*:main causing CLI routing to wrong thread-bound session; patched compiled dist/*.js; manual bundle edits with reapply script." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core bugs (per-turn metadata duplication causing context growth and session resolver fallback routing to agent:*:main) with relevant technical terms; minor mention of patched dist files is implementation detail but doesn't harm deduping." - }, - "usage": { - "input_tokens": 650, - "output_tokens": 744 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "OpenClaw fails to send Discord reply messages: outgoing replies error with \"TypeError: fetch failed\" despite Discord proxy being configured and login succeeding; incoming messages and model responses are received/generated correctly.", - "solution_summary": "Ensure the Discord client's REST requests use the configured proxy (e.g., pass a proxy/agent to fetch or honor HTTP(S)_PROXY env vars) so reply POSTs go through the proxy; add handling/fallback when proxy application fails.", - "maintainer_signal_summary": "Regression in 2026.3.2 on Node.js 22/systemd; logs show \"rest proxy enabled\" and \"gateway proxy enabled\" and successful login, but only final reply fetch fails; curl through the same proxy works, indicating the proxy is not being applied to outgoing reply requests in code.", - "dedupe_summary": "discord fetch failed reply proxy-configured rest-proxy gateway-proxy enabled regression openclaw-2026.3.2 node22 systemd" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Captures the core problem (Discord reply send failing with 'fetch failed' despite rest/gateway proxy enabled) using useful technical terms, but includes noisy version/platform metadata that should be removed for cleaner deduping." - }, - "usage": { - "input_tokens": 590, - "output_tokens": 704 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "CLI command 'openclaw agent --channel discord --to ...' routes messages to agent:*:main instead of the live Discord channel/thread session because the session resolver doesn't lookup by delivery context or normalize Discord targets.", - "solution_summary": "Patch the session resolver to normalize Discord targets and perform a lookup by delivery context (channel + to + accountId) before falling back to agent:*:main; change applied to dist/gateway-cli-BjsM6fWb.js as a manual fix.", - "maintainer_signal_summary": "Bug exists in compiled dist file (dist/gateway-cli-BjsM6fWb.js) in v2026.3.11; manual patch has high regression risk and will be overwritten by updates—related issues #36401 and #41562.", - "dedupe_summary": "CLI agent discord session resolver delivery context lookup normalization fallback agent:*:main dist/gateway-cli-BjsM6fWb.js v2026.3.11" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary concisely identifies the core bug (CLI Discord session resolver not normalizing delivery context, causing fallback to agent:*:main) and cites the affected compiled file, but includes an unnecessary version tag which is nonessential for deduping." - }, - "usage": { - "input_tokens": 655, - "output_tokens": 625 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "In v2026.3.11 a regression causes ReferenceError at runtime because dist/compact-1mmJ_KWL.js calls a nonexistent resolveAgentMainSessionKey instead of buildAgentMainSessionKey, breaking the agent command and possibly crashing the gateway.", - "solution_summary": "Manually replace resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...) in dist/compact-1mmJ_KWL.js and add the fix into the source/build so it persists across reinstalls.", - "maintainer_signal_summary": "High-risk manual patch on compiled dist file; this is a regression of fix #3 and must be fixed in the source or build pipeline to avoid losing the patch when reapplying fix #3 or updating OpenClaw.", - "dedupe_summary": "resolveAgentMainSessionKey buildAgentMainSessionKey ReferenceError dist/compact-1mmJ_KWL.js v2026.3.11 regression session-resolver fix#3" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary concisely identifies the exact missing function, the replacement function, the ReferenceError and the affected compiled file, but includes minor noise like the release version and 'fix#3' tag that aren't needed for deduping." - }, - "usage": { - "input_tokens": 626, - "output_tokens": 604 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "Discord channel plugin applies channels.discord.proxy only to WebSocket connections; REST API HTTP requests (used for health checks and message send) ignore the proxy, causing 'fetch failed' errors and timeouts in proxy-required environments.", - "solution_summary": "Make the Discord plugin's HTTP client honor channels.discord.proxy for all REST API calls (health check and message send), e.g., pass proxy config to the HTTP transport or use a shared proxied client; add tests and fix config handling.", - "maintainer_signal_summary": "Regression observed in OpenClaw 2026.3.8: health check reports 'fetch failed' despite UI showing ON|OK; curl with same proxy succeeds, indicating REST calls bypass the configured proxy and break CLI message send.", - "dedupe_summary": "discord REST proxy ignored; channels.discord.proxy; websockets proxied; healthcheck 'fetch failed'; message send timeout; regression 2026.3.8" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Captures the core issue (Discord REST calls not using channels.discord.proxy while websockets are, causing healthcheck 'fetch failed' and message timeouts) but includes an unnecessary regression version/date which is noise." - }, - "usage": { - "input_tokens": 582, - "output_tokens": 688 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the session resolver inside the agent command in dist/compact-1mmJ_KWL.js does not lookup sessions by delivery context (channel+to+accountId), causing sessions to resolve to agent:*:main and responses to be misassociated.", - "solution_summary": "Patched the compiled file dist/compact-1mmJ_KWL.js to perform a delivery-context lookup (channel+to+accountId) in the agent command session resolver and only fallback to main when no specific session exists.", - "maintainer_signal_summary": "Bug exists in a compiled dist file (dist/compact-1mmJ_KWL.js) on v2026.3.11; fix applied manually to dist, so it will be lost on reinstall/update — high regression risk.", - "dedupe_summary": "dist/compact-1mmJ_KWL.js session resolver missing delivery-context lookup (channel+to+accountId) → resolves to agent:*:main; manual compiled-file fix added delivery-context lookup; v2026.3.11, high-regression-risk" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Clear, specific summary of the core bug (missing delivery-context lookup causing resolution to agent:*:main) and the manual fix, but includes unnecessary metadata (version and regression-risk) that aren't needed for deduplication." - }, - "usage": { - "input_tokens": 596, - "output_tokens": 615 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "The Control UI contains most interface text hardcoded in English (labels, buttons, statuses, tooltips), embedded even in built JS bundles, blocking non-English users from effective use.", - "solution_summary": "Extract all hardcoded strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, provide a language selector or auto-detect, and create infrastructure to add more locales later.", - "maintainer_signal_summary": "OpenClaw docs already provide docs/zh-CN; control-ui dist bundles contain hardcoded strings like “Settings”/“Add”/“Delete”; proposer flags Medium-High UX priority and offers to contribute—asks if this is on the roadmap.", - "dedupe_summary": "control-ui hardcoded-English strings; extract i18n; add zh-CN locale" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it identifies the control-ui's hardcoded English strings and the intended i18n extraction plus adding a zh-CN locale, making it ideal for clustering localization requests." - }, - "usage": { - "input_tokens": 517, - "output_tokens": 585 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "WhatsApp channel only supported a root-level systemPrompt, preventing per-account or per-group prompt customization and wildcard fallback behavior for groups.", - "solution_summary": "Extend WhatsApp config/schema to support systemPrompt at account and group levels, implement resolution hierarchy (account overrides root; group resolves specific → wildcard; account groups replace root groups) and compose final injected prompt as account + resolved group separated by a blank line; add tests and docs.", - "maintainer_signal_summary": "Changes touch WhatsApp config types/schema, account resolution, group prompt composition, unit tests and documentation; no changes to non-WhatsApp channels, hot-reload, or group allowlist/mention-gating semantics; closes related issue.", - "dedupe_summary": "whatsapp systemPrompt account-level group-level wildcard groups['*'] account.groups replace root.groups resolution-hierarchy combine account+group config schema tests docs" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Captures the core change (adding WhatsApp systemPrompt at account and group levels, wildcard groups, resolution/merge and config/schema updates) but is terse and includes minor noisy mentions of tests/docs rather than full sentences." - }, - "usage": { - "input_tokens": 2293, - "output_tokens": 502 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages sent via messages.upsert(type=\"append\") were dropped before agent inbound processing, and WhatsApp group policy resolution could ignore account-scoped settings (groupAllowFrom/allowFrom).", - "solution_summary": "Enable append processing only for WhatsApp group messages (DM append still skipped) and propagate accountId so account-scoped groupAllowFrom/allowFrom are used when resolving WhatsApp group policy defaults.", - "maintainer_signal_summary": "Changes are scoped to WhatsApp inbound and group-activation code (monitor.ts and group-activation helpers): enable group append handling, propagate accountId to policy resolution; no schema/dist/debug changes and DM append remains skipped; tests and live repro validated.", - "dedupe_summary": "whatsapp group append dropped messages.upsert inbound; policy resolved without accountId ignoring account-scoped groupAllowFrom/allowFrom allowlist gating" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise summary captures the two core issues (WhatsApp group 'append' messages being dropped and policy resolution ignoring account-scoped allowlist gating), though the phrasing is slightly terse and could be clearer." - }, - "usage": { - "input_tokens": 1845, - "output_tokens": 637 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because resolveClientIp returned undefined for trusted-proxy requests lacking X-Forwarded headers and isLocalDirectRequest did not fall back to the socket address when allowRealIpFallback was enabled.", - "solution_summary": "Make resolveClientIp return req.socket.remoteAddress when no forwarding headers are present; update isLocalDirectRequest to check req.socket.remoteAddress when allowRealIpFallback=true; gate .ts.net host acceptance behind auth.allowTailscale and thread that flag through callers; update docs and tests.", - "maintainer_signal_summary": "Adds unit tests (src/gateway/auth.test.ts, src/gateway/net.test.ts), modifies gateway auth/net/server files, preserves default allowTailscale=true when tailscale.mode='serve', closes #28472 and depends on PR #50636; verified end-to-end and documents required trustedProxies and allowRealIpFallback settings.", - "dedupe_summary": "Tailscale Serve Canvas 401; resolveClientIp lacked socket-address fallback; isLocalDirectRequest missing req.socket.remoteAddress fallback; .ts.net host acceptance gated by allowTailscale." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary of the root causes and fixes for Canvas 401 behind Tailscale Serve that omits boilerplate and would match similar proxy/auth fallback issues." - }, - "usage": { - "input_tokens": 1684, - "output_tokens": 763 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "checkInboundAccessControl() computed isSelfChat but never used, so WhatsApp self-chat deployments with account.selfChatMode true allowed non-self messages (groups, stranger DMs, broadcasts) because groupPolicy defaulted to \"open\", violating self-chat isolation.", - "solution_summary": "Add an early-return guard in checkInboundAccessControl() that, when account.selfChatMode === true, blocks all non-self messages (returns allowed:false) before any group/DM policy evaluation, while allowing owner messages via isSamePhone and keeping isSelfChat computed for UI/logging.", - "maintainer_signal_summary": "Change limited to src/web/inbound/access-control.ts; guard placed before policy resolution; auto-detected heuristic (isSelfChatMode()) unchanged by design; all related access-control tests pass; revert by removing the early-return guard.", - "dedupe_summary": "WhatsApp self-chat not enforced; checkInboundAccessControl isSelfChat unused; non-self messages allowed due to groupPolicy default open; fix: early-return guard on account.selfChatMode true blocks non-self (uses isSamePhone to allow owner); file src/web/inbound/access-control.ts; tests pass." - }, - "judge": { - "score": 4, - "has_boilerplate": true, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the bug, root cause, and fix (mentions checkInboundAccessControl, groupPolicy default, and the guard), but includes minor noise like a file path and 'tests pass'." - }, - "usage": { - "input_tokens": 1399, - "output_tokens": 721 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "With groupPolicy: \"allowlist\", group messages from senders not in groupAllowFrom are dropped at the access-control layer and never stored as context, so the agent misses those messages in its contextual history despite docs expecting them to be stored for context-only.", - "solution_summary": "Add an opt-in groupContextFromAll flag (top-level or per-account, default false) that makes access-control return storeForContext for non-allowlisted group senders; those messages are marked contextOnly, recorded in group history, and do not trigger replies or send read receipts.", - "maintainer_signal_summary": "Modifies access-control.ts (adds storeForContext), inbound monitor (contextOnly handling, skip read receipts), types, auto-reply on-message (recordPendingHistoryEntryIfEnabled), config types and Zod schema, and accounts resolution; default false preserves backward compatibility; watch allowlist normalization and potential gating/state-update bypasses as merge risks.", - "dedupe_summary": "WhatsApp groupPolicy allowlist drops non-allowlisted messages; add groupContextFromAll flag to storeForContext→contextOnly via access-control+monitor; store pending context, no-reply, no-read-receipts, groupAllowFrom, mention-gating" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summarizes the core bug (allowlist groupPolicy drops non-allowlisted messages) and the proposed fix (add groupContextFromAll to store messages as pending context via access-control/monitor), but includes a few minor extra terms (no-reply, no-read-receipts) that are slightly noisy." - }, - "usage": { - "input_tokens": 1416, - "output_tokens": 839 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "Add an optional sessionKey to sessions_spawn to let sub-agents reuse deterministic sessions instead of new UUIDs; short keys are namespaced to agent:{agentId}:subagent:{key}, fully-qualified keys are validated against targetAgentId, otherwise fallback to crypto.randomUUID().", - "solution_summary": "Implement sessionKey handling (short-key namespacing, fully-qualified key passthrough with agentId validation), add tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch, and patch allowlist bypass.", - "maintainer_signal_summary": "Tests currently fail to compile due to wrong import (openclaw-tools.js vs clawdbot-tools.js); validation has a case-sensitivity bug (embedded agentId not normalized) causing false rejections; inconsistent error handling uses throw in one place vs jsonResult elsewhere—fix tests, normalize IDs, and unify error responses.", - "dedupe_summary": "sessions_spawn sessionKey reuse deterministic session short-key namespace agent:{agentId}:subagent:{key} fully-qualified key validation cross-agent mismatch case-sensitivity normalizeAgentId inconsistent error handling throw jsonResult broken test import openclaw-tools.js clawdbot-tools.js allowlist bypass tests" - }, - "judge": { - "score": 3, - "has_boilerplate": false, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Contains the core change (sessions_spawn sessionKey for deterministic sub-agent sessions, short-key namespace mapping, fully-qualified validation, and allowlist bypass fix) but includes noisy test/import/error-handling details and implementation minutiae that reduce signal clarity." - }, - "usage": { - "input_tokens": 1469, - "output_tokens": 913 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Editing Telegram messages with media (photo, video, audio, document, animation) used editMessageText and caused Telegram 400 error “there is no text in the message to edit”, so caption edits silently failed and left UI inconsistent.", - "solution_summary": "editMessageTelegram now catches that specific 400 (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption for captionable media; buttons-only updates use editMessageReplyMarkup; MESSAGE_NOT_MODIFIED is treated as success and interface is unchanged.", - "maintainer_signal_summary": "Change limited to src/telegram/send.ts, 6 new unit tests cover retry, buttons-only, error propagation and MESSAGE_NOT_MODIFIED; backward compatible with no API surface changes; risk noted that regex may break if Telegram error text changes.", - "dedupe_summary": "telegram editMessageText 400 \"no text in the message to edit\" -> retry editMessageCaption; captionable media photo video audio document animation; editMessageReplyMarkup for buttons-only; NO_TEXT_IN_MESSAGE_RE; src/telegram/send.ts" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core fix: catch Telegram's 'no text in the message to edit' 400, retry with editMessageCaption for captionable media, use editMessageReplyMarkup for buttons-only, and points to the implementation location." - }, - "usage": { - "input_tokens": 1350, - "output_tokens": 510 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "PR #24962 hardcoded disableBlockStreaming: true for WhatsApp, causing all messages to be batched and preventing config-driven block streaming (e.g., blockStreamingDefault:on, blockStreamingBreak:text_end).", - "solution_summary": "Modify deliver callback to suppress only tool payloads and drive disableBlockStreaming from per-account channels.whatsapp.blockStreaming with fallback to agents.defaults.blockStreamingDefault, restoring progressive block streaming while keeping reasoning suppressed upstream.", - "maintainer_signal_summary": "Live WhatsApp verification and unit tests pass; reasoning/tool suppression remains enforced upstream; change is backward-compatible and reversible via channels.whatsapp.blockStreaming:false.", - "dedupe_summary": "whatsapp hardcoded disableBlockStreaming PR24962 blocked blockStreaming config channels.whatsapp.blockStreaming agents.defaults.blockStreamingDefault deliver callback suppressed everything fixed to suppress only tool payloads shouldSuppressReasoningPayload upstream" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary captures the core change: WhatsApp's disableBlockStreaming was hardcoded and blocked config-driven block streaming, now restored to use per-account config and the deliver callback only suppresses tool payloads because reasoning is filtered upstream." - }, - "usage": { - "input_tokens": 1342, - "output_tokens": 627 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Sending videos >5MB caused the Telegram gateway to hang because getFile/content-length checks left the HTTP response body unconsumed, causing undici Agent.close to deadlock in closeDispatcher.", - "solution_summary": "Increased default channels.telegram.mediaMaxMb from 5 to 20; fixed closeDispatcher by adding a timeout and destroy fallback to avoid Agent.close deadlock; added timeouts/retries to getFile and download logic; made media download failures non-fatal; stripped inbound media path metadata from UI.", - "maintainer_signal_summary": "Changes touch src/infra/net/ssrf.ts and src/telegram/bot.ts; default mediaMaxMb changed (configurable); tests updated (strip-inbound-meta.test.ts passing); no SSRF policy or new-dependency changes. To revert quickly, reset channels.telegram.mediaMaxMb to 5 or restore the modified files.", - "dedupe_summary": "telegram gateway hang video unconsumed response body undici Agent.close deadlock closeDispatcher timeout destroy fallback mediaMaxMb 5→20 getFile content-length exceeds maxBytes retries timeouts non-fatal media errors" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that highlights the root cause (undici Agent.close deadlock from unconsumed response body) and key fixes (increase mediaMaxMb, timeouts/retries, non-fatal media errors), so it will cluster with similar Telegram media/gateway hang issues." - }, - "usage": { - "input_tokens": 1438, - "output_tokens": 881 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "Telegram document attachments with no caption were misidentified because resolveTelegramInboundBody hardcoded the fallback body text as , causing agents to treat documents as images (e.g., PDFs reported as not received).", - "solution_summary": "Use the precomputed type-aware placeholder in resolveTelegramInboundBody so the fallback body emits , , etc., and add unit tests validating media placeholders and document handling.", - "maintainer_signal_summary": "Bug fix in extensions/telegram; adds unit tests (helpers.test.ts, bot.create-telegram-bot.test.ts); closes #7116; no changes to media download or file-extraction logic; backward compatible with narrow edge-case risk.", - "dedupe_summary": "telegram document placeholder mislabel hardcoded instead of type-aware in resolveTelegramInboundBody fallback" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the bug and code location (resolveTelegramInboundBody) describing the wrong hardcoded placeholder and correct type-aware , with no boilerplate noise." - }, - "usage": { - "input_tokens": 1265, - "output_tokens": 590 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "MEDIA_MAX_BYTES (5 MB) was hardcoded in src/media/store.ts, causing files larger than 5 MB to silently fail staging into sandboxed containers.", - "solution_summary": "Introduce optional media.maxBytes config (default 5 MB, max 500 MB), replace hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(), read setting at gateway boot, update error messages and tests accordingly.", - "maintainer_signal_summary": "Closes #40880; unit tests pass; addressed bot review comments; backward compatible (default preserved, deprecated constant still exported); schema enforces 500 MB cap; changes touch gateway boot and media store code.", - "dedupe_summary": "media.maxBytes config replaces hardcoded MEDIA_MAX_BYTES (5MB); getMediaMaxBytes/setMediaMaxBytes used at gateway boot; schema max 500MB; store.ts and error messages updated." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, noise-free summary that captures the core change—making MEDIA_MAX_BYTES configurable via media.maxBytes with getter/setter usage at gateway boot and related schema/store/error updates—so it will cluster with similar config/limit changes." - }, - "usage": { - "input_tokens": 1386, - "output_tokens": 797 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "When Telegram media/file download fails (due to size limit or API error) the handler returned early and the message was silently dropped, so the agent received no ack and users got no feedback.", - "solution_summary": "On download failure extract file metadata (name, size, type) from the Telegram message, inject a metadata stub into msg.text, and allow the message to continue through processMessage instead of returning early.", - "maintainer_signal_summary": "Integration change in src/telegram/bot-handlers.ts: replace early returns on download errors with buildFileMetadataStub and msg.text mutation; normal media download path, media groups, stickers, and outbound sending unchanged and backward compatible.", - "dedupe_summary": "Telegram media download failure silent drop; inject file metadata stub into msg.text and continue pipeline; fixes >mediaMaxMb (default 5MB) and Telegram getFile 20MB limit; bot-handlers.ts return-on-error removed." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core change—prevents silent drops when Telegram media download fails by injecting file metadata into msg.text and continuing the pipeline (includes relevant size-limit context and removed early return)." - }, - "usage": { - "input_tokens": 1228, - "output_tokens": 665 - } - }, - { - "thread_id": 5796, - "number": 24117, - "kind": "pull_request", - "title": "feat(cli): add --session-key targeting for openclaw agent", - "summary": { - "problem_summary": "openclaw agent has no --session-key option, so CLI-triggered deliveries often rely on unstable UUID sessionId or sender-derived --to and can route to the wrong (agent main) session in automation/callback flows.", - "solution_summary": "Add a --session-key CLI flag to openclaw agent, add sessionKey to AgentCliOpts and forward it into resolveSessionKeyForRequest/gateway, update help/validation, and add tests; session-key takes precedence.", - "maintainer_signal_summary": "Code changes in src/cli/program/register.agent.ts and src/commands/agent-via-gateway.ts, tests added in register.agent.test.ts and agent-via-gateway.test.ts; local tests pass; change is additive and backward-compatible with no core resolver semantics altered.", - "dedupe_summary": "openclaw agent --session-key CLI flag stable session targeting forwarded via AgentCliOpts->resolveSessionKeyForRequest->gateway precedence over --session-id/--to/--agent tests" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that names the new --session-key flag, its purpose (stable session targeting), implementation path (AgentCliOpts->resolveSessionKeyForRequest->gateway) and precedence over other flags, making it ideal for clustering similar changes." - }, - "usage": { - "input_tokens": 1200, - "output_tokens": 864 - } - }, - { - "thread_id": 766, - "number": 11611, - "kind": "pull_request", - "title": "feat: separate group-level allowlist from sender-level command authorization", - "summary": { - "problem_summary": "With groupPolicy set to \"allowlist\", the system currently gates all group interactions by sender phone number via groupAllowFrom, preventing group-level JID allowlisting, owner-only command restriction, and blocking spam groups while allowing open chat in approved groups.", - "solution_summary": "When groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access is gated by group JIDs from that mapping (via resolveChannelGroupPolicy), allowing any participant in approved groups to chat while retaining groupAllowFrom for slash-command authorization; falls back to sender-based filtering when no groups mapping is present.", - "maintainer_signal_summary": "Code change in src/web/inbound/access-control.ts to use resolveChannelGroupPolicy, docs update in src/config/types.whatsapp.ts, new tests in access-control.group-allowlist.test.ts; depends on #3326 and preserves legacy sender-based behavior when groups config absent.", - "dedupe_summary": "WhatsApp groupPolicy:allowlist uses channels.whatsapp.groups (group JID allowlist) for inbound gating; groupAllowFrom still restricts slash commands; fallback to sender-based filtering when no groups mapping; key funcs: checkInboundAccessControl, resolveChannelGroupPolicy; prevents spam groups while allowing open chat in approved groups." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change (use group JID allowlist for inbound gating, preserve sender-based command auth, fallback behavior) without boilerplate and would match similar issues." - }, - "usage": { - "input_tokens": 1250, - "output_tokens": 622 - } - }, - { - "thread_id": 17924, - "number": 46425, - "kind": "pull_request", - "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", - "summary": { - "problem_summary": "Telegram account-configured mediaMaxMb was ignored for outbound reply messages because loadWebMedia() in the reply delivery path received no maxBytes and fell back to the 100 MB default.", - "solution_summary": "Thread mediaMaxBytes through the Telegram outbound reply delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia() receives configured maxBytes; add unit tests.", - "maintainer_signal_summary": "Small, backwards-compatible bugfix limited to Telegram integration; touches five delivery-path files, adds two unit tests, all related test suites pass; closes #46023.", - "dedupe_summary": "telegram mediaMaxMb ignored outbound replies; loadWebMedia maxBytes missing; threaded mediaMaxBytes through bot.ts, bot-message.ts, bot-message-dispatch.ts, delivery.replies.ts, bot-native-commands.ts; unit tests added." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise and specific about the bug (mediaMaxMb ignored for Telegram outbound replies) and the code paths/files changed, but includes a minor testing detail ('unit tests added') that isn't needed for deduping." - }, - "usage": { - "input_tokens": 1251, - "output_tokens": 438 - } - }, - { - "thread_id": 5712, - "number": 37050, - "kind": "pull_request", - "title": "fix(telegram): add HTML parse fallback for media captions", - "summary": { - "problem_summary": "Telegram media sends (photo/document/video/animation/audio/voice) lacked an HTML-parse fallback, so captions containing invalid HTML/entities caused Telegram to return a 400 and the media send to fail.", - "solution_summary": "Wrap the media send callback with withTelegramHtmlParseFallback so on an HTML parse error the send is retried without parse_mode using the raw caption; applied to all media types.", - "maintainer_signal_summary": "A unit test was added to assert first call uses parse_mode:'HTML' and the retry omits parse_mode; change is limited to the media send inner callback (text/edit/thread/caption-splitting logic unchanged) and can be reverted by restoring src/telegram/send.ts.", - "dedupe_summary": "telegram media caption HTML-parse-error → 400; add withTelegramHtmlParseFallback retry without parse_mode for sendPhoto/sendDocument/sendVideo/sendAnimation/sendAudio/sendVoice" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the HTML-parse-error causing 400 for media captions and the added withTelegramHtmlParseFallback retry (no parse_mode) across the listed send* media methods, which is ideal for deduplication." - }, - "usage": { - "input_tokens": 1125, - "output_tokens": 723 - } - }, - { - "thread_id": 21769, - "number": 55280, - "kind": "pull_request", - "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", - "summary": { - "problem_summary": "WhatsApp had block streaming effectively disabled (hardcoded/incorrectly wired) and a race in the block-reply delivery pipeline caused intermediate blocks to be lost or final replies suppressed, producing silent turns.", - "solution_summary": "Make blockStreaming configurable per-account/global, add sensible coalescing defaults, introduce dispatcher.sendBlockReplyAsync() to await confirmed delivery before marking sentContentKeys, and fix the disableBlockStreaming undefined bug; tests updated.", - "maintainer_signal_summary": "Updates require dispatcher API support for sendBlockReplyAsync in mocks/tests, config key channels.whatsapp.blockStreaming (default false) now respected, and the change fixes the root cause of silent turns rather than the prior workaround; existing tests modified/added.", - "dedupe_summary": "whatsapp blockStreaming configurable delivery race sendBlockReplyAsync sentContentKeys disableBlockStreaming undefined bug block-reply pipeline" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Contains the core technical signals (WhatsApp blockStreaming configurability and a delivery-race bug in the block-reply pipeline) but is terse and keywordy rather than a short, clear 1–3 sentence summary." - }, - "usage": { - "input_tokens": 1038, - "output_tokens": 513 - } - }, - { - "thread_id": 8098, - "number": 25543, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked support for per-group allowlist entries, so specific Signal groups could not be explicitly allowed to bypass sender-level groupAllowFrom checks or run control commands.", - "solution_summary": "Introduce channels.signal.groups. via a new SignalGroupConfig and Zod schema, wire resolveChannelGroupPolicy into the Signal event handler and commandGate so listed groups bypass groupAllowFrom and are authorized for commands, and update docs.", - "maintainer_signal_summary": "Implementation follows existing channel patterns (Telegram/iMessage/etc.), TypeScript and Zod validations pass, addresses prior review feedback, and is low-risk and safe to merge.", - "dedupe_summary": "signal channels.signal.groups group-level allowlist groupAllowFrom bypass resolveChannelGroupPolicy commandGate SignalGroupConfig Zod-schema" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise, technical keywords capture the core change (adding a group-level allowlist/config for Signal and related code/schema hooks) but is terse and could be a one-sentence description rather than a keyword list." - }, - "usage": { - "input_tokens": 980, - "output_tokens": 583 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel treated groupAllowFrom entries as sender identifiers, so group IDs were not recognized and group messages were silently dropped when groupPolicy was set to \"allowlist\".", - "solution_summary": "Add a groups config to SignalAccountSchemaBase and call resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom; include 4 tests to verify behavior.", - "maintainer_signal_summary": "Small change (size S) targeting channel: signal; fixes #25540; adds schema entry, event-handler patch, and tests; all relevant tests pass.", - "dedupe_summary": "signal groups-config missing resolveChannelGroupPolicy group-level-allowlist failure groupAllowFrom-as-sender silent-drop" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies Signal lacked a group-level allowlist (missing groups config/resolveChannelGroupPolicy) causing groupAllowFrom to be misapplied as a sender filter and drop group messages, which is sufficient to cluster similar issues." - }, - "usage": { - "input_tokens": 780, - "output_tokens": 707 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "WhatsApp channel currently hardcodes disableBlockStreaming: true in process-message.ts, causing intermediate agent message blocks to be accumulated and delivered as a single final WhatsApp message instead of separate block-streamed messages.", - "solution_summary": "Make WhatsApp respect channels.whatsapp.blockStreaming by adding resolveWhatsAppBlockStreamingEnabled(), replacing the hardcoded disable flag, treating intermediate blocks as valid replies when enabled, registering blockStreaming and DEFAULT_BLOCK_STREAMING_COALESCE in the WhatsApp dock, and updating tests.", - "maintainer_signal_summary": "Changes touch process-message.ts, dock.ts, and process-message.inbound-contract.test.ts; tests updated and all existing tests pass; small change (size: S); behavior remains backwards-compatible when blockStreaming is false.", - "dedupe_summary": "WhatsApp disableBlockStreaming hardcoded; add channels.whatsapp.blockStreaming; resolveWhatsAppBlockStreamingEnabled; intermediate-block delivery vs single-batch; dock register DEFAULT_BLOCK_STREAMING_COALESCE; process-message.ts" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core change (remove hardcoded disableBlockStreaming for WhatsApp, add channels.whatsapp.blockStreaming, implement resolveWhatsAppBlockStreamingEnabled, and allow intermediate block deliveries) and references the relevant files, so it will cluster with similar PRs/issues." - }, - "usage": { - "input_tokens": 530, - "output_tokens": 767 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v5-component-focused.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v5-component-focused.json deleted file mode 100644 index 7eed1cb..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v5-component-focused.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v5-component-focused", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "Summarize this GitHub issue or pull request for automated duplicate detection. Your summary will be embedded and clustered.\n\nStructure your analysis:\n1. First identify the COMPONENT or SUBSYSTEM (e.g., \"Discord gateway\", \"WhatsApp delivery\", \"Telegram media handler\", \"CLI routing\", \"session management\")\n2. Then identify the SPECIFIC PROBLEM or CHANGE within that component\n3. Combine into a clear dedupe_summary that starts with the component name\n\nIgnore completely: template boilerplate, testing instructions, checklists, environment info, reproduction steps, deployment notes, version numbers, cross-references.\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nPlain text, no markdown, 1-3 sentences each.\ndedupe_summary format: \"[Component]: [specific issue or change]\" — this helps cluster by subsystem.", - "timestamp": "2026-03-30T17:20:46.303Z", - "aggregate": { - "avg_score": 4.97, - "avg_signal_density": 4.97, - "boilerplate_count": 0, - "boilerplate_pct": 0, - "would_cluster_correctly_pct": 100, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 45423, - "total_output_tokens": 24588 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "Discord channel proxying (channels.discord.proxy) only applies to the WebSocket Gateway; REST API calls (command deploy, fetch bot identity) and guild resolution are not routed through the configured HTTP proxy, causing fetch failures, unresolved guild channels, and no guild message handling when the host requires an HTTP proxy.", - "solution_summary": "Apply the configured proxy to the undici dispatcher used for all Discord REST calls and the guild-resolve code path (not just the Gateway WS). Consider a global proxy config that sets a ProxyAgent for undici, and add retries/error handling for guild resolution.", - "maintainer_signal_summary": "Startup logs show WS connects via proxy but REST calls time out with \"fetch failed\"; setting a global undici ProxyAgent fixes REST but not guild resolve; CLI guild resolve succeeds when bootstrapped with ProxyAgent; issue is a regression impacting users behind HTTP proxies and reproducible on local macOS with a proxy tool.", - "dedupe_summary": "Discord channel (channels.discord.proxy): REST API and guild resolution are not proxied—only the WebSocket is—breaking guild channels behind an HTTP proxy." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states the exact subsystem (channels.discord.proxy), the technical failure (REST API/guild resolution not proxied while WebSocket is), and the user impact, so it will cluster correctly with similar proxy-related Discord channel issues." - }, - "usage": { - "input_tokens": 2271, - "output_tokens": 563 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "Component: Discord channels/RequestClient. Outbound REST API calls for sending messages bypass the configured channels.discord.proxy, causing fetch to fail in proxied/restricted networks (user reports TypeError: fetch failed while WebSocket gateway receiving works). The Carbon RequestClient’s fetch implementation appears to have no proxy support, leading to message-send failures in regions that require a proxy.", - "solution_summary": "Ensure REST/send requests honor channels.discord.proxy by adding proxy support to the RequestClient/fetch layer (support HTTP/SOCKS/TUN as applicable) and route attachment downloads/uploads through the configured proxy. Update RequestClient to accept and apply proxy configuration or use a proxy-capable HTTP client so REST operations use the same proxy as the gateway.", - "maintainer_signal_summary": "User repro in a China/proxy-required environment with logs noting rest proxy enabled but REST not using proxy; gateway (WebSocket) works while REST fails, blocking message sends. Labeled bug and high impact for users in restricted networks—prevents normal bot replies and media operations.", - "dedupe_summary": "Discord channels (REST/RequestClient): channels.discord.proxy not applied to outbound REST sends; RequestClient.fetch lacks proxy support" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific description that the channels.discord.proxy config isn't used for REST requests because RequestClient.fetch has no proxy support, which will match similar bug reports." - }, - "usage": { - "input_tokens": 953, - "output_tokens": 675 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "Discord provider (OpenClaw) behind an HTTP proxy partially starts: gateway/WebSocket login succeeds and application ID fetches, but some REST startup calls consistently fail with “fetch failed” (notably native command deployment and bot identity fetch), making the provider appear half-working.", - "solution_summary": "Ensure all Discord REST request paths use the same proxy-aware fetch/dispatcher as the gateway/login path (e.g., centralize HTTP client or attach proxy agent to global fetch), add tests for proxy coverage across startup code paths, and fix any code paths bypassing configured proxy.", - "maintainer_signal_summary": "Logs show rest proxy and gateway proxy enabled, applicationId fetch succeeds, but deploy-rest and fetch-bot-identity fail repeatedly with \"fetch failed\"; issue reproducible by user and likely high-impact because provider appears logged in but REST features are broken. User can provide environment details or test patches.", - "dedupe_summary": "Discord provider: REST startup calls (native commands deployment and bot identity fetch) fail with 'fetch failed' when HTTP proxy is configured, while gateway/login succeeds." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core failure (Discord REST startup calls for command deployment and bot identity fetch failing with a proxy while gateway/login works), so it will match similar reports." - }, - "usage": { - "input_tokens": 1099, - "output_tokens": 523 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "Discord channel proxy is logged as enabled but the REST API and gateway connections fail with \"fetch failed\" when OpenClaw runs under WSL2 using an HTTP proxy. The proxy setting (~/.openclaw/openclaw.json channels.discord.proxy) appears recognized in logs but is not applied to the underlying Discord.js/fetch client or WebSocket, leaving gatewayConnected=false.", - "solution_summary": "Ensure the configured HTTP proxy is actually passed to Discord.js/undici/fetch and the WebSocket client (e.g., instantiate and attach an HTTP/HTTPS/WS proxy agent or set the appropriate undici/global agent), so REST calls and gateway connections use the proxy under WSL2. Add tests and logging to verify fetch/gateway use the proxy and handle proxy-specific edge cases in WSL2.", - "maintainer_signal_summary": "Reporter marks severity as high: Discord channel completely non-functional for users behind an HTTP proxy (100% reproducible in WSL2). Logs show proxy enabled messages but repeated fetch failures; workaround is using a different channel or TUN-mode transparent proxy; config and curl verification included.", - "dedupe_summary": "Discord channel: proxy logged as enabled but REST/gateway requests fail with \"fetch failed\" because the Discord client is not applying the configured HTTP proxy (WSL2)" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states that although the Discord proxy is logged as enabled, the client isn't applying the configured HTTP proxy (WSL2) causing REST/gateway 'fetch failed' errors, which is ideal for deduplication." - }, - "usage": { - "input_tokens": 936, - "output_tokens": 728 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "Discord channel REST API calls ignore configured proxy (channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY env vars) and fail with \"TypeError: fetch failed\" when sending messages, while the WebSocket connection and other channels (e.g., Telegram) work through the proxy and curl via proxy succeeds.", - "solution_summary": "Route Discord REST API requests through the same proxy-aware fetch/SSRF guard path used for web tools (web_search/web_fetch) so the gateway honors channels.discord.proxy and standard HTTP_PROXY/HTTPS_PROXY settings for outbound REST calls.", - "maintainer_signal_summary": "Logs show Discord final reply failed with fetch error; changelog notes a recent proxy-aware fix for web tools but explicitly excludes Discord REST API; user provided reproducible steps and curl proof that proxy itself works, and offered a suggested fix and a limited workaround (webhooks).", - "dedupe_summary": "Discord REST API: REST calls ignore configured proxy (channels.discord.proxy and env vars) causing fetch failures when sending messages; WebSocket and other channels use proxy correctly." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug — Discord REST calls ignore configured proxy (both config and env vars) causing fetch failures while WebSocket still uses the proxy — with no boilerplate or irrelevant details." - }, - "usage": { - "input_tokens": 959, - "output_tokens": 497 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "CLI session resolver for the agent command incorrectly routes Discord deliveries to the generic agent:*:main session instead of the live channel/thread session because it does not lookup sessions by real delivery context (channel + to + accountId), fails to normalize Discord targets, and the command path did not propagate necessary context.", - "solution_summary": "Applied manual patches to compiled dist files to add delivery-target-based session resolution, normalize Discord targets (e.g., prefix channel IDs), add lookup by delivery context in the agent resolver, fix use of buildAgentMainSessionKey, and pass channel and replyAccount/accountId through the command path; validated CLI now routes to correct channel sessions.", - "maintainer_signal_summary": "High-risk manual edits were made directly in dist/ compiled files and will be lost on updates; maintainers should port fixes into source, add tests for delivery-context session resolution, and include a post-update validation step to prevent regressions.", - "dedupe_summary": "CLI session resolver: 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel/thread session due to missing delivery-context lookup and Discord target normalization" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that states the exact CLI command misrouting and the specific root causes (missing delivery-context lookup and Discord target normalization), so it will reliably match duplicates." - }, - "usage": { - "input_tokens": 886, - "output_tokens": 644 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions in Discord session handling: (1) volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) was being re-injected on every turn, causing progressive context bloat and degraded thread performance; (2) CLI agent commands routed to the wrong session (defaulting to agent:*:main) instead of the live thread-bound session.", - "solution_summary": "Patched runtime bundles to stop reinjecting volatile Discord thread metadata; updated CLI/gateway code to resolve sessions by actual delivery context (normalizing Discord targets and using channel + to + accountId), passed delivery context into the session resolver, and fixed a regression by switching back to buildAgentMainSessionKey where appropriate.", - "maintainer_signal_summary": "Modified compiled/runtime bundle files in openclaw/dist (pi-embedded, gateway-cli, compact, register.agent); manual patches were applied to compiled bundles and will be lost on update—maintainers should reapply or provide a persistent fix; verification shows threads no longer carry repeated context and CLI routes to the correct session.", - "dedupe_summary": "Discord gateway/thread session management: prevent reinjection of volatile Discord thread metadata each turn and fix CLI routing to resolve sessions by actual delivery target (channel + to + accountId)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the two core issues—repeated injection of volatile Discord thread metadata and incorrect CLI session resolution by delivery context—without boilerplate, so it will cluster correctly." - }, - "usage": { - "input_tokens": 805, - "output_tokens": 680 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "Component: tools/sandbox (sandbox media staging). The MEDIA_MAX_BYTES constant is hardcoded to 5 MB in built files (dist/store-*.js), causing files larger than 5 MB to be rejected or silently fail when copied into sandboxed agents.", - "solution_summary": "Expose the media staging cap as a user-configurable setting (e.g., tools.media.maxBytes in openclaw.json) and have the sandbox loader read that config instead of using the hardcoded 5 MB constant; ensure build artifacts reflect the configurable value.", - "maintainer_signal_summary": "Reporter marks this a regression (worked before), present since v2026.3.7 and still in v2026.3.8, with a manual sed patch workaround applied after npm install -g; labels: bug, regression; no explicit severity provided.", - "dedupe_summary": "tools/sandbox: MEDIA_MAX_BYTES is hardcoded to 5 MB in dist/store-*.js and should be made configurable via openclaw.json (tools.media.maxBytes)" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the hardcoded 5 MB limit in dist/store-*.js and the exact desired config key (tools.media.maxBytes), which is ideal for deduplication." - }, - "usage": { - "input_tokens": 852, - "output_tokens": 546 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "Discord REST API requests fail with TypeError 'fetch failed' when the gateway tries to deploy commands or send messages, even though proxy is configured in openclaw.json and logs show 'rest proxy enabled'. The WebSocket gateway connection works and Telegram with same proxy works; direct curl calls through the proxy succeed.", - "solution_summary": "Ensure the Discord REST client uses the configured proxy for HTTP(S) requests (e.g., attach a proxy agent to Node's fetch/undici or honor HTTP(S)_PROXY), so REST calls route through the proxy like curl and stop producing 'fetch failed'.", - "maintainer_signal_summary": "Issue includes gateway logs showing 'rest proxy enabled' and repeated 'fetch failed' errors; bot receives messages but cannot send; curl with same proxy succeeds; environment: macOS, Node.js 25.6.1, OpenClaw build noted; labeled as a bug.", - "dedupe_summary": "Discord REST API: REST requests fail with 'fetch failed' despite proxy being configured and shown enabled, while gateway WebSocket and curl+proxy work correctly." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific core problem statement: REST API calls error with 'fetch failed' despite proxy configured and enabled, while gateway and curl+proxy succeed, which is exactly the signal needed to cluster similar reports." - }, - "usage": { - "input_tokens": 797, - "output_tokens": 740 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "Discord REST API requests made via @buape/carbon's RequestClient (and other fetch-based code paths like memorySearch and attachment downloads) ignore the channels.discord.proxy setting in openclaw.json because RequestClient calls globalThis.fetch without a proxy-aware dispatcher; Node.js 22 native fetch also doesn't honor HTTP_PROXY/HTTPS_PROXY without --use-env-proxy.", - "solution_summary": "Change @buape/carbon's REST client to accept and use an explicit dispatcher or proxy option, and have OpenClaw pass channels.discord.proxy into the client so REST calls use the same proxy as the WebSocket gateway; alternatively document usage of an undici EnvHttpProxyAgent preload as a temporary workaround (avoid NODE_OPTIONS=--use-env-proxy).", - "maintainer_signal_summary": "This is a cross-cutting proxy handling bug affecting multiple fetch-based code paths (REST requests, embeddings, attachment downloads) due to Node 22 fetch behavior; a library-level change in @buape/carbon plus a configuration pass-through from OpenClaw is the preferred fix; workaround exists via setGlobalDispatcher with undici.", - "dedupe_summary": "Discord REST client (@buape/carbon RequestClient): ignores channels.discord.proxy and calls globalThis.fetch without a proxy-aware dispatcher" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: names the component (@buape/carbon RequestClient), the config key (channels.discord.proxy), and the observed behavior (uses globalThis.fetch without a proxy-aware dispatcher), so it cleanly captures the core issue for deduplication." - }, - "usage": { - "input_tokens": 806, - "output_tokens": 516 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "OpenClaw suffers performance degradation under sustained load with long-running conversations: unbounded context token growth (writer ~42k tokens, reasoning ~90k), Discord lane blocking causing serialization under load, and typing indicators that can remain stuck until TTL.", - "solution_summary": "Applied configuration limits and timeouts: reduced max concurrent sessions/agents/subagents and children, set agents.defaults.contextTokens to 80000, increased channels.discord.eventQueue.listenerTimeout to 300000, restarted the gateway, and added an anti-regression patching script to cover reply/compact/pi/gateway-cli/register variants.", - "maintainer_signal_summary": "Gateway restarted and running; no new 'lane wait exceeded' or 'typing TTL reached' errors observed post-restart; existing dist patches intact; restart mitigates but does not fully eliminate issues under sustained load; recommendation to expose adaptive or tunable limits via openclaw config.", - "dedupe_summary": "session management: unbounded context token growth and lane blocking under load — enforced context token limits, reduced concurrency settings, and increased Discord event queue listener timeout." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core issue (unbounded context token growth and lane blocking under load) and the specific mitigations (context limits, reduced concurrency, increased Discord event queue timeout) with no boilerplate, so it will cluster correctly." - }, - "usage": { - "input_tokens": 740, - "output_tokens": 739 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every conversation turn, contaminating context, increasing token usage, and breaking prompt caching because the runtime lacks a guard to limit injection to the first turn.", - "solution_summary": "A manual patch was applied to compiled dist files: in dist/pi-embedded-D6PpOsxP.js metadata injection for Discord threads was prevented, and in dist/compact-1mmJ_KWL.js an isNewSession guard was added and resolveAgentMainSessionKey was replaced with buildAgentMainSessionKey(...). Validation shows sessions and prompt caching work again, and thread context no longer repeats.", - "maintainer_signal_summary": "High regression risk — fixes were made directly in compiled artifacts (dist/), so changes will be lost on update; maintainers should apply the fix in source, add tests/guards for first-turn-only injection, and run post-update revalidation. Affected compiled files: dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js.", - "dedupe_summary": "Discord threads: runtime re-injects volatile thread metadata every turn (message_id, reply_to_id, timestamp, Thread starter, Chat history), causing context pollution and prompt cache bust." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core bug (per-turn re-injection of volatile Discord thread metadata and its impact) with concrete fields listed, no boilerplate or noise, so it will cluster correctly with similar reports." - }, - "usage": { - "input_tokens": 752, - "output_tokens": 686 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "Component: Discord threads. The runtime in the compiled dist treats Discord threads as needing volatile metadata injected every turn (message_id, reply_to_id, timestamp, Thread starter, Chat history), causing context pollution, repeated history, degraded performance, and prompt-cache busting with increased token costs.", - "solution_summary": "A local manual patch edited dist/pi-embedded-D6PpOsxP.js to stop injecting those volatile fields on every turn and only inject them on the first turn; validation shows repeated context stops, prompt caching is effective, and billed tokens drop significantly.", - "maintainer_signal_summary": "High regression risk: the fix was applied to a compiled dist file and will be overwritten by updates; maintainers should apply an official source-level fix and/or add a post-update revalidation script. Related issues: #41355, #34228, #37047.", - "dedupe_summary": "Discord threads: volatile thread metadata re-injected on every turn causing context pollution, token bloat and cache busting; temporary compiled-file patch made to inject metadata only on first turn." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: states the core bug (Discord thread metadata re-injected each turn), its impacts (context pollution, token bloat, cache busting), and notes the targeted fix without template noise, so it will cluster with similar reports." - }, - "usage": { - "input_tokens": 760, - "output_tokens": 517 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "Discord threads runtime had two regressions: per-turn reinjection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing progressive context growth and performance/latency degradation, and the session resolver falling back to agent:*:main instead of resolving live thread-bound sessions leading to incorrect CLI routing.", - "solution_summary": "Manual patches applied to compiled runtime bundles (modified dist/*.js files) to stop metadata reinjection and enforce resolver lookup by delivery context (channel+to+accountId); backups created and an idempotent re-application script added, gateway restarted and validated.", - "maintainer_signal_summary": "Fixes are manual edits to compiled dist bundles and will be lost on updates—maintainers must keep and run the provided re-apply script and backups; verification checklist passed and recommended non-regression tests (long-thread, 15–30min monitoring, smoke tests) should be added.", - "dedupe_summary": "[Discord threads]: Runtime patches to prevent per-turn metadata reinjection causing context growth and to fix session resolver so CLI routes to thread-bound sessions" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the two core runtime fixes (prevent per-turn metadata reinjection causing context growth and correct CLI session resolver to use thread-bound sessions) with no boilerplate or noise." - }, - "usage": { - "input_tokens": 736, - "output_tokens": 842 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "Discord message sending (REST) failing to deliver replies: OpenClaw receives messages and generates responses, but final reply fails with \"TypeError: fetch failed\" despite Discord proxy being configured and reported as enabled.", - "solution_summary": "Ensure the Discord REST client uses the configured proxy for outgoing reply fetch calls (apply proxy agent to fetch requests), add clearer logging around which proxy/agent is applied to REST requests, and implement retries or better error handling for fetch failures; add tests for proxy use in reply sending.", - "maintainer_signal_summary": "Regression: previously worked, now fails on OpenClaw 2026.3.2 with Node.js 22 on Linux systemd; proxy (http://127.0.0.1:7890) verified working via curl; logs show discord rest/gateway proxy enabled and login succeeded but replies consistently fail with \"final reply failed: TypeError: fetch failed\" in both DMs and group chats—high impact for Discord functionality.", - "dedupe_summary": "Discord message sending: outgoing replies fail with 'TypeError: fetch failed' despite discord proxy configured/enabled (incoming messages received and responses generated)" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (outgoing Discord replies failing with 'fetch failed' despite proxy being configured while incoming messages and responses are generated), with no boilerplate or noise." - }, - "usage": { - "input_tokens": 676, - "output_tokens": 683 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "Component: CLI session resolver. The CLI command `openclaw agent --channel discord --to ...` routes messages to `agent:*:main` instead of the live Discord channel/thread because the compiled resolver does not look up sessions by delivery context and does not normalize Discord targets.", - "solution_summary": "A manual patch was applied to the compiled file `dist/gateway-cli-BjsM6fWb.js` to normalize Discord targets (e.g., prefix `channel:`), perform session lookup by delivery context (channel+to+accountId) before falling back, and ensure CLI messages route to the correct live session; validations confirm correct routing.", - "maintainer_signal_summary": "High-risk manual fix applied to a compiled `dist/` artifact that will be lost on upgrade; needs a source-level fix and tests to prevent regression. Related issues: #36401 and #41562.", - "dedupe_summary": "CLI routing/session resolver: CLI --channel discord --to resolves to agent:*:main instead of the live Discord session because the resolver lacks delivery-context lookup and target normalization; patched compiled dist file as a stopgap." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: states that the CLI's Discord routing resolves to agent:*:main due to missing delivery-context lookup and target normalization and notes a patched dist file, which is exactly the core issue for deduplication." - }, - "usage": { - "input_tokens": 741, - "output_tokens": 854 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "Component: session resolver/compact bundle. A regression in the compiled file dist/compact-1mmJ_KWL.js calls a nonexistent resolveAgentMainSessionKey, causing ReferenceError at runtime, breaking the `agent` command and potentially crashing the gateway.", - "solution_summary": "Manual patch applied to dist/compact-1mmJ_KWL.js replacing resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...), which removes the ReferenceError and restores agent/sessions behavior; note this is a high-risk change to compiled files and will be lost on update.", - "maintainer_signal_summary": "This is a regression introduced by the prior session-resolver fix and must be fixed in source (not only in dist) to prevent recurrence; current fix is a temporary manual edit of compiled output and should be integrated into upstream code/release.", - "dedupe_summary": "session resolver (compact): ReferenceError from using nonexistent resolveAgentMainSessionKey instead of buildAgentMainSessionKey in dist/compact-1mmJ_KWL.js" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the root cause (wrong function name in compiled compact bundle causing ReferenceError) that will match similar issues and contains no boilerplate." - }, - "usage": { - "input_tokens": 712, - "output_tokens": 641 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "Discord channel plugin regression: when channels.discord.proxy is configured the proxy is only used for WebSocket connections, not for REST API calls or health checks. As a result the health check reports \"fetch failed\" and openclaw message send to Discord times out or fails in proxy-required environments despite curl via the same proxy working.", - "solution_summary": "Apply the channels.discord.proxy setting to the HTTP client used for Discord REST API calls and health checks (not just WebSocket), ensuring all Discord HTTP requests are routed through the configured proxy; update tests and health-check behavior to validate proxy usage.", - "maintainer_signal_summary": "Reporter marks this a regression and provides reproduction steps showing direct curl with the proxy succeeds while OpenClaw fails; includes version and environment details and labels bug/regression, indicating it breaks message delivery in proxy environments.", - "dedupe_summary": "Discord plugin: channels.discord.proxy is only applied to WebSocket connections, not REST API calls or health checks, causing fetch failures and message-send timeouts via proxy." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the plugin, exact config key, and the core bug (proxy applied only to WebSocket, not REST/health), which will cluster correctly with similar reports." - }, - "usage": { - "input_tokens": 668, - "output_tokens": 566 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "Session resolver for the agent command in the compact build (dist/compact-1mmJ_KWL.js) omits delivery-context lookup (channel+to+accountId), so sessions fall back to agent:*:main and agent responses are not associated with the live channel/thread session.", - "solution_summary": "A manual patch to dist/compact-1mmJ_KWL.js adds lookup by delivery context (channel + to + accountId) in the agent session resolver and only falls back to main when no specific session is found; validation shows sessions and routing are correct after the change.", - "maintainer_signal_summary": "High-regression risk: change was made directly in compiled dist file and will be lost on reinstall/update; requires a source-level fix and release. Patch validated locally but maintainers should implement the fix in source and publish a new build.", - "dedupe_summary": "session resolver (agent command, compact build): missing delivery-context lookup (channel+to+accountId) causing fallback to agent:*:main; manual dist patch added to perform delivery-context lookup." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies the affected component (agent command in compact build), the root cause (missing delivery-context lookup channel+to+accountId) and the symptom (fallback to agent:*:main), with only a minor note about a manual patch." - }, - "usage": { - "input_tokens": 682, - "output_tokens": 683 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English, creating usability barriers for non-English users (notably Chinese speakers) across settings, agent/skill management, channel configuration, and status messages.", - "solution_summary": "Proposed extraction of hardcoded strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, add a language selector or auto-detect, and build infrastructure to easily add more languages; notes that docs already have zh-CN translations and that control-ui bundles contain hardcoded strings.", - "maintainer_signal_summary": "This is a feature request with medium-high priority for UX accessibility; the author asks whether this is on the roadmap, offers contribution interest, and highlights specific files/bundles containing hardcoded strings.", - "dedupe_summary": "Control UI localization: Add i18n support and zh-CN translations by extracting hardcoded English strings into locale files, adding a language selector/auto-detect, and creating infrastructure for additional languages." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and focused summary that clearly captures the core change — add i18n/locales and zh-CN plus UI language selection — with no boilerplate or irrelevant details." - }, - "usage": { - "input_tokens": 603, - "output_tokens": 487 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "Component: WhatsApp channel/group prompt handling. Problem: WhatsApp group chats lacked support for channel-configured systemPrompt at account and group levels, preventing per-account and per-group prompt customization and wildcard defaults.", - "solution_summary": "Added systemPrompt support to WhatsApp config types/schema and implemented resolution logic: channel root or account-level systemPrompt (account overrides root), then resolve group prompt by specific group entry or '*' wildcard within the active groups map; account-level groups fully replace root groups. The final injected prompt is account prompt plus resolved group prompt (joined with a blank line). Tests and docs were added.", - "maintainer_signal_summary": "Scope limited to the WhatsApp channel only; no changes to other channels, WhatsApp hot-reload behavior, or group allowlist/mention-gating semantics. Includes tests and documentation and closes the related issue. Account-groups replace root groups (same semantics as Telegram).", - "dedupe_summary": "WhatsApp: add systemPrompt support at account and group levels with wildcard fallback and account-groups replacing root-groups" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary capturing the core change—adding per-account and per-group WhatsApp systemPrompt support with wildcard fallback—without extraneous template or testing details." - }, - "usage": { - "input_tokens": 2379, - "output_tokens": 708 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before agent processing, and WhatsApp group policy resolution could ignore account-scoped configuration causing allowlist gating to fail.", - "solution_summary": "Scope append processing to WhatsApp group messages only and propagate accountId when resolving WhatsApp group policy so account-scoped groupAllowFrom/allowFrom and groups fallback are considered, restoring correct inbound routing and gating.", - "maintainer_signal_summary": "DM append history remains skipped; no config/schema/dist changes; targeted local tests and live repro were used to verify behavior; files touched include web inbound and group-activation monitors and risks are documented for group backlog processing and account-scoped policy differences.", - "dedupe_summary": "WhatsApp group handling: enable append processing for group messages and use account-scoped accountId when resolving group allowlist/policy" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: clearly states enabling append processing for WhatsApp group messages and switching to account-scoped accountId for group allowlist/policy resolution without extraneous boilerplate." - }, - "usage": { - "input_tokens": 1931, - "output_tokens": 463 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Gateway (Canvas/Tailscale Serve): Canvas/A2UI returned 401 when proxied through Tailscale Serve because resolveClientIp returned undefined for trusted-proxy requests missing forwarding headers and isLocalDirectRequest did not fall back to the socket address when allowRealIpFallback was enabled.", - "solution_summary": "resolveClientIp now falls back to req.socket.remoteAddress when no forwarding headers exist; isLocalDirectRequest also checks the socket address when allowRealIpFallback is set; isLocalishHost now requires an allowTailscale flag to accept .ts.net hosts; auth.allowTailscale is threaded through callers and docs updated with required config.", - "maintainer_signal_summary": "Includes unit tests added/updated (src/gateway/auth.test.ts, net.test.ts) with passing results, docs changes, and an end-to-end verification matrix; closes #28472 and depends on #50636; no new config keys introduced and default behavior preserved for tailscale.mode = \"serve\", with noted risks and mitigations.", - "dedupe_summary": "Gateway: Fix Canvas 401 behind Tailscale Serve by falling back to socket address for trusted-proxy requests and gating .ts.net host acceptance behind allowTailscale." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core bug (401 behind Tailscale Serve) and the two code changes (socket fallback for trusted-proxy requests and gating .ts.net acceptance via allowTailscale) without boilerplate." - }, - "usage": { - "input_tokens": 1770, - "output_tokens": 503 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "Component: WhatsApp inbound access control computed an isSelfChat heuristic but never used it as a guard, so non-self messages (groups, stranger DMs, broadcasts) could pass through when groupPolicy defaulted to \"open\", violating self-chat isolation.", - "solution_summary": "Added an early-return guard in checkInboundAccessControl that rejects all non-self messages when account.selfChatMode === true, placed before group/DM policy evaluation; the isSelfChat heuristic is still returned for UI/logging but not used to block messages, and auto-detected self-chat remains unaffected.", - "maintainer_signal_summary": "All relevant tests pass (9 tests); author verified guard placement via code-path analysis and checked edge cases; decision to require explicit selfChatMode (not the heuristic) is intentional to avoid blocking teammates; quick revert instructions and affected file (src/web/inbound/access-control.ts) provided.", - "dedupe_summary": "WhatsApp inbound access control: add early-return guard to block non-self messages when explicit selfChatMode=true" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the code change: an early-return guard added to WhatsApp inbound access control to block non-self messages when explicit selfChatMode=true, with no boilerplate or extraneous detail." - }, - "usage": { - "input_tokens": 1485, - "output_tokens": 672 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "Component: WhatsApp group inbound handling. Problem: With groupPolicy: \"allowlist\", group messages from senders not in groupAllowFrom are dropped at the access-control layer and never stored as context, so agents lack conversation context from non-allowlisted members.", - "solution_summary": "Adds an opt-in boolean config groupContextFromAll (top-level and per-account) that, when true, marks non-allowlisted group messages as storeForContext/contextOnly instead of dropping them; these messages are saved to pending group history, do not trigger replies, and skip read receipts. Changes update access-control, inbound monitor, message types, auto-reply logic, and config schema.", - "maintainer_signal_summary": "Backwards-compatible default false; per-account config overrides top-level. Local testing with two bots verified context retention. Reviewer notes caution about potential allowlist normalization edge cases and interaction with group gating/state updates.", - "dedupe_summary": "WhatsApp group handling: add groupContextFromAll option to store non-allowlisted group messages as pending context instead of dropping them" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core change—adding a groupContextFromAll option to retain non-allowlisted WhatsApp group messages as pending context rather than dropping them—without extraneous boilerplate." - }, - "usage": { - "input_tokens": 1502, - "output_tokens": 489 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "Component: session management / sub-agent spawn. Current sessions_spawn always created a new random-UUID session for each sub-agent spawn, preventing reuse of conversation history and enabling session-key bypass edge cases.", - "solution_summary": "Added an optional sessionKey parameter to sessions_spawn that maps short keys to namespaced sessions (agent:{agentId}:subagent:{key}), accepts fully-qualified keys with agentId validation, and falls back to crypto.randomUUID() when omitted; tests added for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch; allowlist bypass fixed.", - "maintainer_signal_summary": "Reviewer flagged three actionable issues: a broken test import (openclaw-tools.js vs clawdbot-tools.js) causing test compilation failure, a case-sensitivity bug in fully-qualified key validation (embedded agentId not normalized causing false mismatches), and inconsistent error handling (throw vs jsonResult) for cross-agent mismatch checks—these should be fixed before merge.", - "dedupe_summary": "session management: add sessionKey param to sessions_spawn to enable deterministic sub-agent sessions (namespaced short keys, validated fully-qualified keys, UUID fallback); tests added but includes a failing import and validation/error-handling bugs." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary concisely captures the core change (sessionKey for deterministic sub-agent sessions, namespacing/validation, UUID fallback) but includes a testing/failing-import note that is implementation/test noise." - }, - "usage": { - "input_tokens": 1555, - "output_tokens": 636 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Telegram message editing: calling message(action=edit) on messages containing media (photo, video, audio, document, animation) invoked editMessageText and Telegram returned 400 'there is no text in the message to edit', causing edits to captionable messages to fail and leave UI inconsistent.", - "solution_summary": "editMessageTelegram (src/telegram/send.ts) now catches the specific 400 error (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption for captionable media; buttons-only updates call editMessageReplyMarkup directly; HTML parse fallback and MESSAGE_NOT_MODIFIED handling preserved; six unit tests added.", - "maintainer_signal_summary": "Bug fix limited to Telegram adapter (src/telegram/send.ts); backward compatible with no API surface changes; adds calls to editMessageCaption and editMessageReplyMarkup and a regex to detect the Telegram error—monitor regex stability; 6 tests added; revert by restoring the modified file.", - "dedupe_summary": "Telegram message editing: auto-detect captionable media edits and retry with editMessageCaption (buttons-only use editMessageReplyMarkup) to avoid 400 'no text' errors." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary specifying the exact Telegram API methods (editMessageCaption, editMessageReplyMarkup) and the 400 'no text' error, so it accurately captures the core issue for clustering." - }, - "usage": { - "input_tokens": 1436, - "output_tokens": 710 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "WhatsApp delivery: PR #24962 hardcoded disableBlockStreaming: true to block reasoning leaks, which unintentionally disabled block streaming entirely so messages were batched and sent only when the agent finished, ignoring per-account or global blockStreaming settings.", - "solution_summary": "The deliver callback was narrowed to only suppress tool payloads (reasoning is already filtered upstream) and disableBlockStreaming is now read from per-account channels.whatsapp.blockStreaming, falling back to agents.defaults.blockStreamingDefault, restoring progressive block streaming at configured boundaries.", - "maintainer_signal_summary": "Live testing and contract tests pass; change is backward compatible and can be reverted by setting channels.whatsapp.blockStreaming:false or removing the default; reviewers should watch for any regression of the upstream shouldSuppressReasoningPayload filter.", - "dedupe_summary": "WhatsApp delivery: restore config-driven block streaming by respecting channels.whatsapp.blockStreaming and agents.defaults.blockStreamingDefault and limiting deliver suppression to tool payloads." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it captures the exact behavioral change (restore config-driven block streaming for WhatsApp), names the relevant config keys, and notes the narrowed deliver suppression to tool payloads, with no boilerplate noise." - }, - "usage": { - "input_tokens": 1428, - "output_tokens": 458 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Telegram gateway integration: sending videos larger than the old 5MB default caused fetch/download to throw on content-length checks and left the undici Agent hanging in closeDispatcher() when the response body was unconsumed, making the gateway unresponsive and dropping messages with media.", - "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher() to avoid Agent.close() deadlock (timeout + destroy fallback), made media download errors non-fatal so captions/text are still forwarded, and added timeouts/retries to getFile and download operations; updated UI metadata stripping for inbound media paths.", - "maintainer_signal_summary": "E2E verified with a 7.5MB video (saved to ~/.openclaw/media/inbound and forwarded), tests updated (strip-inbound-meta.test.ts passes), change is backward-compatible and configurable, no SSRF policy change or new deps; touched src/infra/net/ssrf.ts and src/telegram/bot.ts; revertable by lowering channels.telegram.mediaMaxMb or reverting commits; noted risk of 3s closeDispatcher timeout but mitigated by destroy().", - "dedupe_summary": "Telegram integration: fix gateway hang on video messages by increasing mediaMaxMb to 20, preventing Agent.close() deadlock, making media download errors non-fatal, and adding timeouts/retries" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core bug and fixes (increase media limit, fix Agent.close deadlock, non-fatal media errors, timeouts/retries) without extraneous template or testing details." - }, - "usage": { - "input_tokens": 1524, - "output_tokens": 739 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "Telegram inbound body fallback used a hardcoded when media (e.g., documents) had no caption, causing documents and other non-photo media to be misidentified to the agent and leading to incorrect agent responses.", - "solution_summary": "Changed resolveTelegramInboundBody to use the already-computed type-aware placeholder (e.g., , ) instead of hardcoded , and added unit tests verifying placeholder text and getFile behavior for document messages; media download and extraction paths unchanged.", - "maintainer_signal_summary": "This is a small bugfix in the Telegram integration with unit tests added in extensions/telegram tests, closes #7116, backward compatible with minimal risk (noted downstream pattern-match caveat), and verified by test suite runs; labeled channel: telegram, size: S.", - "dedupe_summary": "Telegram inbound body: replace hardcoded fallback with type-aware media placeholder so document and other non-photo attachments are correctly labeled (e.g., ), plus unit tests to lock behavior." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core fix—switching the Telegram inbound body placeholder from a hardcoded to a type-aware placeholder (so documents/videos are labeled correctly) and adding tests—no extraneous boilerplate." - }, - "usage": { - "input_tokens": 1351, - "output_tokens": 516 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "Media store/gateway enforces a hardcoded 5 MB limit (MEDIA_MAX_BYTES) in src/media/store.ts, causing files larger than 5 MB to fail staging into sandboxed agents and preventing users from overriding the limit.", - "solution_summary": "Introduce a new config field media.maxBytes (optional, up to 500 MB), replace the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(), read config at gateway boot, and update error messages to reflect the configured limit while preserving the 5 MB default.", - "maintainer_signal_summary": "All relevant tests pass (56 tests across four files); three bot review threads were resolved (unconditional setting of limit, improved byte formatting, schema validation); change is backward-compatible (default preserved, deprecated export kept) and schema caps max at 500 MB to mitigate disk-fill risk.", - "dedupe_summary": "Media store: make MEDIA_MAX_BYTES configurable via media.maxBytes, replacing hardcoded 5MB with a configurable limit (default 5MB, max 500MB) and updating error messages." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change—making the media size limit configurable (keeps 5MB default, introduces up to 500MB) and updates related errors—without template noise." - }, - "usage": { - "input_tokens": 1472, - "output_tokens": 473 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "Telegram integration: when media/file downloads fail (exceeding mediaMaxMb or Telegram API errors), the handler returned early and the entire message was silently dropped—agents received no ack or visibility into the lost file or caption.", - "solution_summary": "On download failure, extract file metadata (name, size, type) from the Telegram message object and inject a metadata stub into msg.text instead of returning; allow the message to continue through the normal pipeline so the agent sees the file and reason it couldn't be downloaded.", - "maintainer_signal_summary": "Bug fix touching the Telegram integration only; backward compatible with no config changes. Verified via code inspection of early-return paths; risk noted that mutating msg.text may affect downstream consumers but is limited to prior-error cases. Can be reverted by rolling back the single commit to src/telegram/bot-handlers.ts; related issues linked.", - "dedupe_summary": "Telegram integration: on Telegram media download failure, inject file metadata into msg.text and continue processing instead of dropping the message" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states the integration, the failure mode (media download), and the exact behavior change (inject metadata into msg.text and continue processing) without extraneous boilerplate." - }, - "usage": { - "input_tokens": 1314, - "output_tokens": 522 - } - }, - { - "thread_id": 5796, - "number": 24117, - "kind": "pull_request", - "title": "feat(cli): add --session-key targeting for openclaw agent", - "summary": { - "problem_summary": "openclaw agent CLI lacks a --session-key option, causing CLI-triggered deliveries in multi-channel setups to resolve to the agent main session instead of the intended channel session because sessionId is UUID-based and --to may be unavailable for callbacks.", - "solution_summary": "Adds a first-class --session-key CLI flag, extends AgentCliOpts to include sessionKey, forwards it into resolveSessionKeyForRequest and gateway call parameters, preserves resolution precedence (--session-key, --session-id, --to, --agent), and updates help/validation text with accompanying tests.", - "maintainer_signal_summary": "Change is opt-in and backward compatible, includes unit tests for CLI parsing and gateway forwarding, judged low risk with a 5/5 confidence score, and requires no core resolver changes; documentation/help updated accordingly.", - "dedupe_summary": "[CLI agent command]: Add --session-key flag to target stable session keys for openclaw agent; forwarded through session resolution and prioritized over session-id/to/agent." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core change (adding a --session-key flag to openclaw agent and its precedence in session resolution) with relevant technical terms and no template noise." - }, - "usage": { - "input_tokens": 1286, - "output_tokens": 740 - } - }, - { - "thread_id": 766, - "number": 11611, - "kind": "pull_request", - "title": "feat: separate group-level allowlist from sender-level command authorization", - "summary": { - "problem_summary": "Component: WhatsApp inbound group access control. The current groupPolicy: \"allowlist\" combined with groupAllowFrom gates all group interactions by sender phone number, preventing allowlisting by group JID, allowing open chat within approved groups, and separating slash-command authorization from general chat access.", - "solution_summary": "When groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, access control uses the groups mapping to gate group-level access by JID (blocking unapproved groups) while allowing any participant in approved groups to chat; groupAllowFrom still controls slash-command authorization. If no groups mapping is present, the legacy sender-based groupAllowFrom behavior is preserved.", - "maintainer_signal_summary": "Change is localized to src/web/inbound/access-control.ts with added resolveChannelGroupPolicy usage, config JSDoc update, and four new Vitest cases. Tests cover approved/blocked groups and legacy fallback; flagged dependency on PR #3326; reviewer noted one outdated inline comment but rates change as safe to merge with confidence ~4/5.", - "dedupe_summary": "WhatsApp group access control: Use channels.whatsapp.groups JID allowlist for group-level gating when groupPolicy is \"allowlist\", while keeping groupAllowFrom for command authorization and preserving legacy sender-based behavior when no groups mapping exists." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change: switching to a group JID allowlist for group-level gating while retaining sender-based command authorization and legacy behavior when no groups mapping exists." - }, - "usage": { - "input_tokens": 1336, - "output_tokens": 613 - } - }, - { - "thread_id": 17924, - "number": 46425, - "kind": "pull_request", - "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", - "summary": { - "problem_summary": "Telegram outbound reply delivery ignored the configured mediaMaxMb limit: loadWebMedia was called without maxBytes and defaulted to 100 MB, so operators setting a lower mediaMaxMb saw no effect for replies (direct send already honored the setting).", - "solution_summary": "Threaded mediaMaxBytes through the Telegram outbound delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts), added unit tests to verify maxBytes is passed or omitted appropriately, and left direct-send and internals unchanged.", - "maintainer_signal_summary": "Bug fix, backward compatible (mediaMaxBytes optional), two unit tests added and related test suites passing (108 tests across 4 suites); build and checks pass but no live end-to-end Telegram verification was performed; change can be reverted by reverting the commit.", - "dedupe_summary": "Telegram outbound delivery: propagate mediaMaxMb/mediaMaxBytes to outbound media reply delivery so loadWebMedia enforces configured limit (threaded through bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts and native-commands path)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that states the core bugfix (propagating mediaMaxMb/Bytes so loadWebMedia honors the configured limit) and lists the affected files, with no template noise, so it will cluster correctly." - }, - "usage": { - "input_tokens": 1337, - "output_tokens": 620 - } - }, - { - "thread_id": 5712, - "number": 37050, - "kind": "pull_request", - "title": "fix(telegram): add HTML parse fallback for media captions", - "summary": { - "problem_summary": "Telegram media sends (photo, document, video, animation, audio, voice, video note) would fail with a 400 \"can't parse entities\" error when captions contained problematic HTML entities because they lacked the HTML parse fallback that text messages use.", - "solution_summary": "Wrapped the media send callback in withTelegramHtmlParseFallback to catch HTML parse errors and retry the send without parse_mode using the raw caption; applies to all media types and preserves existing caption splitting and other paths.", - "maintainer_signal_summary": "Added a unit test that simulates a parse error then success, verifying first call uses parse_mode: \"HTML\" and the retry omits parse_mode; all tests pass (52 total). Change is limited to src/telegram/send.ts and can be reverted by restoring the previous sendMedia implementation.", - "dedupe_summary": "Telegram media handler: Add HTML parse fallback for media captions to retry sends as plain text on HTML entity parse errors, preventing 400 failures." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that states the affected component (Telegram media captions), the change (HTML parse fallback to retry as plain text), and the impact (prevents 400 failures), so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 1211, - "output_tokens": 484 - } - }, - { - "thread_id": 21769, - "number": 55280, - "kind": "pull_request", - "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", - "summary": { - "problem_summary": "WhatsApp channel had block-streaming effectively disabled by a hardcoded disableBlockStreaming setting and an activation bug; additionally there was a delivery race where block replies were enqueued optimistically (sendBlockReply) so the pipeline marked content as sent before confirmed delivery, causing intermediate blocks and sometimes the final reply to be lost.", - "solution_summary": "Resolve blockStreaming from account config, fix the config activation logic, add sendBlockReplyAsync that waits for confirmed delivery, update onBlockReply to await it, add sensible block streaming coalescing defaults, and update/add tests to cover streaming-on and streaming-off paths.", - "maintainer_signal_summary": "Existing tests were updated and new tests added; manual verification confirms streaming works when enabled and remains off by default; introduces a new dispatcher API (sendBlockReplyAsync) and a new config key channels.whatsapp.blockStreaming, and fixes the root-cause race rather than reverting behavior.", - "dedupe_summary": "WhatsApp channel: make block streaming configurable and fix delivery race in block-reply pipeline" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies the WhatsApp channel change (make block streaming configurable) and the root fix (delivery race in block-reply pipeline), which is ideal for deduplication." - }, - "usage": { - "input_tokens": 1124, - "output_tokens": 865 - } - }, - { - "thread_id": 8098, - "number": 25543, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacks group-level allowlist config to explicitly allow specific Signal groups by ID, causing reliance on sender-level groupAllowFrom checks and preventing explicitly allowed groups from running control commands.", - "solution_summary": "Added a channels.signal.groups. config with a SignalGroupConfig type and Zod schema, wired resolveChannelGroupPolicy into the Signal event handler and command authorizers so listed groups bypass sender-level checks and can run control commands, and updated docs with examples.", - "maintainer_signal_summary": "PR fixes #25540 and supersedes #4337; Greptile review incorporated (groupExplicitlyAllowed now included in commandGate authorizers), TypeScript compiles cleanly, manual testing done, and changes are low-risk and consistent with other channels.", - "dedupe_summary": "Signal channel: add group-level allowlist support via channels.signal.groups. config that bypasses sender-level groupAllowFrom and enables command authorization for listed groups." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it names the new config path, the behavioral change (bypasses sender-level groupAllowFrom), and the impact on command authorization, so it will accurately cluster with similar Signal group-allowlist changes." - }, - "usage": { - "input_tokens": 1066, - "output_tokens": 484 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Component: Signal channel. Signal lacked group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs, group messages were silently dropped because isSenderAllowed compared sender phone/UUID to group IDs (sender-level filter mismatch).", - "solution_summary": "Added a groups config to SignalAccountSchemaBase (same shape as other channels), invoked resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom, and added four tests; all relevant test suites pass.", - "maintainer_signal_summary": "Small change scoped to the Signal channel; adds schema support and an access-check call, includes 4 new tests, fully tested and fixes issue #25540. Label: channel: signal, size: S.", - "dedupe_summary": "Signal: Missing group-level allowlist support via channels.signal.groups. causing group messages to be dropped when groupPolicy is allowlist; added groups config and resolveChannelGroupPolicy call to fix." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies Signal's missing group-level allowlist, the failure mode (group messages dropped under allowlist), and the fix (add groups config and resolveChannelGroupPolicy), with no boilerplate or noise." - }, - "usage": { - "input_tokens": 866, - "output_tokens": 460 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "WhatsApp channel hardcodes disableBlockStreaming=true in process-message, causing intermediate agent text blocks to be accumulated and sent as one final batch instead of delivered incrementally; queuedFinal was also used exclusively to mark delivery, so intermediate blocks weren't counted as replies.", - "solution_summary": "Add resolveWhatsAppBlockStreamingEnabled() and use the channels.whatsapp.blockStreaming config to control block streaming; update process-message logic so intermediate blocks count as valid replies when enabled; register blockStreaming capability and default coalescing constant in dock and update tests to cover enabled/default/disabled behavior.", - "maintainer_signal_summary": "Backwards-compatible default (blockStreaming off) preserved; tests updated and passing; small scoped change that aligns WhatsApp behavior with Telegram/Line and fixes anyReplyDelivered evaluation; labels indicate whatsapp-web, web-ui, agents, and size S.", - "dedupe_summary": "WhatsApp block streaming: make block streaming config-driven (respect channels.whatsapp.blockStreaming), stop hardcoding disableBlockStreaming, and deliver intermediate blocks as separate messages when enabled." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it captures switching WhatsApp from a hardcoded disableBlockStreaming to respecting channels.whatsapp.blockStreaming and delivering intermediate blocks, which is the core change for clustering." - }, - "usage": { - "input_tokens": 616, - "output_tokens": 623 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v6-negative-examples.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v6-negative-examples.json deleted file mode 100644 index 3f4d519..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v6-negative-examples.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v6-negative-examples", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "Summarize this GitHub issue or pull request. The dedupe_summary field will be used for vector-based duplicate detection.\n\nBAD dedupe_summary examples (too generic, would match unrelated issues):\n- \"Bug fix for an issue with the application\" ← too vague\n- \"Updates configuration handling and fixes a reported problem\" ← no specifics\n- \"Resolves issue where feature doesn't work as expected\" ← says nothing\n\nGOOD dedupe_summary examples (specific, would cluster correctly):\n- \"Discord REST API requests bypass proxy configuration, causing fetch failures in proxy-required environments\"\n- \"WhatsApp group-level systemPrompt configuration not applied to message delivery\"\n- \"Telegram editMessageCaption fails for non-photo media types due to missing content-type detection\"\n\nRemove from your analysis: template headers, checklists, testing instructions, environment details, reproduction steps, deployment notes. These add noise to embeddings.\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nPlain text, no markdown, 1-3 sentences each. dedupe_summary is the most critical field.", - "timestamp": "2026-03-30T17:41:02.635Z", - "aggregate": { - "avg_score": 4.78, - "avg_signal_density": 4.78, - "boilerplate_count": 1, - "boilerplate_pct": 3, - "would_cluster_correctly_pct": 100, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 47183, - "total_output_tokens": 26155 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "channels.discord.proxy only applies to the Discord WebSocket Gateway, not REST API startup requests (deploy commands, fetch-bot-identity) nor the guild resolve path. As a result, in environments that require an HTTP proxy the startup REST calls timeout with \"fetch failed\", guild: remains unresolved, and guild channels are non-functional while DMs (over WebSocket) still work.", - "solution_summary": "Ensure the configured channels.discord.proxy is applied to the undici dispatcher used for all Discord REST API calls during provider startup and the guild-resolve path (not just WebSocket). Consider adding a global proxy option that sets a ProxyAgent (setGlobalDispatcher) and add retry logic for guild resolution.", - "maintainer_signal_summary": "Regression reported on OpenClaw 2026.3.13 with reproduction steps, logs showing REST timeouts and guild unresolved, and verification that curl with the proxy works. User demonstrated a workaround by bootstrapping undici's ProxyAgent (NODE_OPTIONS) which fixes REST but not guild resolve, and suggests code change to route all Discord HTTP traffic through the proxy. Labels: bug, regression.", - "dedupe_summary": "channels.discord.proxy only proxies the Gateway websocket; Node v25/undici REST requests (deploy-rest, fetch-bot-identity) and the guild-resolve code path bypass the proxy causing \"fetch failed\" timeouts and guild channels to be unusable behind an HTTP proxy; global ProxyAgent workaround fixes REST but guild resolve still ignores the proxy." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summarizes the core bug (proxy only applied to Gateway, REST and guild-resolve bypass proxy causing failures) and notes workaround, but includes a Node/undici version detail that is unnecessary for deduping." - }, - "usage": { - "input_tokens": 2315, - "output_tokens": 669 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "With channels.discord.proxy configured, Discord gateway (WebSocket) connects but REST API sends fail with \"fetch failed\" in proxy-required environments (e.g., China). Logs indicate REST proxy enabled but the RequestClient/Carbon fetch implementation doesn't actually use the proxy, causing outbound REST/attachment uploads to fail.", - "solution_summary": "Make the RequestClient/Carbon fetch honor channels.discord.proxy by adding proxy support (HTTP/SOCKS agents or a proxy-aware fetch implementation), ensure attachment downloads/uploads and all REST calls route through the configured proxy, and add tests and configuration parsing so the proxy is consistently applied.", - "maintainer_signal_summary": "Please prioritize a fix to have the Discord REST client use the configured channels.discord.proxy (including attachments) or provide an official workaround; backport to the 2026.2.x line if possible and include tests to prevent regressions for proxy-required regions.", - "dedupe_summary": "channels.discord.proxy not applied to outbound REST sends because Carbon RequestClient's fetch has no proxy support, causing fetch failures in proxy-required environments while WebSocket gateway still works." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that identifies the core bug (channels.discord.proxy not applied to REST) and root cause (Carbon RequestClient fetch lacks proxy support), without boilerplate or noisy details." - }, - "usage": { - "input_tokens": 997, - "output_tokens": 544 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "When OpenClaw is configured with an HTTP proxy for Discord, the provider logs in and the gateway appears healthy but some startup REST calls consistently fail with \"fetch failed\", specifically native command deployment (/applications/{appId}/commands) and bot identity fetch, leaving the provider half-functional and triggering stale-socket health restarts.", - "solution_summary": "Ensure all Discord REST requests use the same proxy-aware fetch/dispatcher as the gateway/WebSocket path (unify transport code paths), and add retry/error handling for startup REST calls; user offered to provide environment details and test a patch.", - "maintainer_signal_summary": "Logs show rest proxy and gateway proxy both enabled, applicationId fetch succeeds but deploy-rest:put and fetch-bot-identity fail with \"fetch failed\" across repeated restarts; issue reproducible on OpenClaw 2026.3.12 (config touched by 2026.3.13) and reporter can share environment or test fixes.", - "dedupe_summary": "OpenClaw Discord provider behind HTTP proxy (channels.discord.proxy=http://127.0.0.1:8119) logs into gateway but startup REST calls—deploying native commands to /applications/{id}/commands and fetching bot identity—fail with \"fetch failed\", indicating inconsistent proxy handling between REST and gateway paths." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Summary concisely describes the core issue—Discord provider logs in via gateway behind an HTTP proxy while REST startup calls to deploy commands and fetch bot identity fail with \"fetch failed\", indicating inconsistent proxy handling—using specific config and endpoints useful for deduplication." - }, - "usage": { - "input_tokens": 1143, - "output_tokens": 694 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "Discord channel proxy is logged as enabled but Discord REST API calls fail with 'fetch failed' when OpenClaw runs under WSL2 with an HTTP proxy; the gateway WebSocket never establishes (gatewayConnected=false).", - "solution_summary": "Ensure the configured channels.discord.proxy is passed to the underlying HTTP client used by discord.js (undici/fetch) and to the WebSocket transport so REST requests and gateway connections use an HTTP proxy agent; update OpenClaw to create and supply a proxy agent or set the appropriate environment/undici agent on startup.", - "maintainer_signal_summary": "Reproducible 100% under WSL2 on OpenClaw 2026.3.13; logs show 'discord: rest proxy enabled' and 'discord: gateway proxy enabled' but REST calls return 'fetch failed' while curl via the same proxy succeeds. Severity: high (Discord channel unusable); config is ~/.openclaw/openclaw.json → channels.discord.proxy; workaround is TUN-mode transparent proxy or alternate channels.", - "dedupe_summary": "Discord proxy configured (channels.discord.proxy) and logged as enabled, but discord.js REST/undici fetch calls fail with 'fetch failed' and the gateway WebSocket never connects under WSL2, despite curl -x to discord.com working; proxy not being applied to the underlying HTTP/fetch/WebSocket client." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "The summary succinctly captures the core bug — the configured Discord proxy isn't being applied to the underlying undici/fetch/WebSocket clients causing REST 'fetch failed' errors and gateway connection failure — with specific tech terms and no template noise." - }, - "usage": { - "input_tokens": 980, - "output_tokens": 790 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "OpenClaw's Discord REST API calls for sending messages ignore the configured proxy and fail with \"TypeError: fetch failed\" while the Discord WebSocket and curl requests through the same proxy work fine, indicating the REST path isn't proxy-aware.", - "solution_summary": "Route Discord REST API calls through the same proxy-aware fetch/SSRF guard path introduced for web_search/web_fetch so they honor channels.discord.proxy and HTTP(S)_PROXY environment variables.", - "maintainer_signal_summary": "Logs show \"discord final reply failed: TypeError: fetch failed\"; repro with channels.discord.proxy and HTTP(S)_PROXY set; Telegram and Discord WebSocket use the proxy successfully and curl POST to the Discord API via the proxy succeeds; changelog notes web tools fixed proxy routing but excluded Discord REST; suggested fix is to reuse the shared proxy-aware fetch path; webhook workaround exists but doesn't support replies.", - "dedupe_summary": "Discord REST API (message send) in OpenClaw bypasses proxy-aware fetch path and fails with \"TypeError: fetch failed\" despite channels.discord.proxy and HTTP(S)_PROXY being set; WebSocket and curl via the proxy succeed, and the 2026.2.26 proxy fix for web_search/web_fetch did not include Discord REST." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that pinpoints Discord REST message sends bypassing the proxy-aware fetch path and failing with a TypeError despite proxy settings, so it will correctly cluster with similar proxy-related Discord REST issues." - }, - "usage": { - "input_tokens": 1003, - "output_tokens": 723 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the CLI command `openclaw agent --channel discord --to ...` resolves and delivers to the global agent:*:main session instead of the live Discord channel/thread session because the CLI session resolver does not look up sessions by real delivery context and Discord targets are not normalized.", - "solution_summary": "A manual patch to compiled dist files was applied: session resolution now searches by delivery context (channel + to + accountId), Discord target IDs are normalized (e.g. '148...' → 'channel:148...'), the compact resolver now uses buildAgentMainSessionKey and looks up delivery context, and the agent register command path now passes channel and replyAccount/accountId.", - "maintainer_signal_summary": "HIGH risk: changes were made directly in compiled files under dist and will be lost on update; maintainers should port fixes to source, add tests for delivery-context session resolution and Discord target normalization, and add post-update validation (or a persistent CLI --session-key option) to prevent regressions.", - "dedupe_summary": "CLI `openclaw agent --channel discord --to` routes to agent:*:main because the session resolver doesn't search by delivery context (channel+to+accountId), the command path didn't propagate channel/replyAccount, and Discord targets weren't normalized (e.g. '148...' vs 'channel:148...'); manual fixes applied to dist/gateway-cli-BjsM6fWb.js, dist/compact-1mmJ_KWL.js, and dist/register.agent-DHoJWl4M.js to add delivery-context lookup and normalization." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that states the exact CLI command misrouting, the root cause (missing delivery-context lookup, command-path propagation, and target normalization), and the files changed, which is sufficient to cluster with similar session-resolver/delivery-context bugs." - }, - "usage": { - "input_tokens": 930, - "output_tokens": 869 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions: Discord thread messages were re-injecting volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) every turn, causing progressive context bloat; and CLI agent commands (openclaw agent --channel discord --to ...) resolved to the default main session (agent:*:main) instead of the live thread-bound session.", - "solution_summary": "Patched compiled runtime bundles to stop injecting the volatile Discord thread metadata, added session resolution by actual delivery target with normalized Discord identifiers, updated the agent command session resolver to look up delivery context (channel + to + accountId), and passed channel and replyAccount/accountId into the resolver; also fixed incorrect session key usage.", - "maintainer_signal_summary": "Changes were applied directly to compiled files in openclaw/dist/ (dist/pi-embedded-*.js, dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js) and will be lost on updates; maintainers should integrate fixes into source, rebuild distribution artifacts, and add a post-update re-application script or permanent release to avoid regression.", - "dedupe_summary": "Discord threads repeatedly re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) each turn causing context degradation, and CLI routing (openclaw agent --channel discord --to ...) resolved to agent:*:main because the session resolver didn't use delivery context (channel+to+accountId); fixes: prevent metadata injection, normalize Discord targets, add delivery-context lookup in agent resolver, pass channel/replyAccount, and correct session key construction in compiled dist/*.js." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary of two related regressions: Discord thread metadata being re-injected each turn (listing exact fields) causing context bloat, and CLI agent commands resolving to the wrong session because the resolver ignored delivery context (channel+to+accountId), with targeted fixes noted." - }, - "usage": { - "input_tokens": 849, - "output_tokens": 732 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "The sandbox media staging cap is hardcoded to 5 MB in dist/store-*.js (const MEDIA_MAX_BYTES = 5 * 1024 * 1024), so files larger than 5 MB are silently rejected or unavailable inside sandboxed agents. Issue present in v2026.3.7 and v2026.3.8.", - "solution_summary": "Read MEDIA_MAX_BYTES from configuration (e.g. openclaw.json under tools.media.maxBytes) instead of a hardcoded constant, aligning with per-type keys like tools.media.image.maxBytes and tools.media.audio.maxBytes; update code to use the configured limit and provide a sensible default.", - "maintainer_signal_summary": "Labels: bug, regression. Affects OpenClaw v2026.3.8 on Ubuntu 24 installed via npm. Reproducible by sending files >5 MB to sandbox.mode non-main. Current workaround: post-install sed patch to replace the constant in dist/store-*.js and restart the gateway.", - "dedupe_summary": "Sandbox media staging limit MEDIA_MAX_BYTES is hardcoded to 5*1024*1024 in dist/store-*.js, causing files over 5 MB to be rejected; request to make tools.media.maxBytes configurable via openclaw.json (issue seen in v2026.3.7 and v2026.3.8)." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise summary captures the hardcoded MEDIA_MAX_BYTES, file location, and requested tools.media.maxBytes config change — the only minor noise is the unnecessary version numbers which aren't needed for deduping." - }, - "usage": { - "input_tokens": 896, - "output_tokens": 614 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "Discord bot receives messages over the Gateway but all Discord REST API calls fail with 'fetch failed' despite proxy configured in openclaw.json and logs showing 'rest proxy enabled'; direct curl calls through the same proxy succeed, so the proxy itself works.", - "solution_summary": "No resolution provided in the issue; user suggests REST client is not using the configured proxy. Investigation needed into how the Discord REST client/fetch is handling per-provider proxy settings (no workaround or patch included).", - "maintainer_signal_summary": "Reporter supplied config, logs and reproduction details (OpenClaw 2026.2.25 on macOS, Node 25.6.1); logs show 'rest proxy enabled' and gateway proxy enabled; no maintainer response or patch in the thread yet.", - "dedupe_summary": "Discord REST API requests return 'fetch failed' even though openclaw.json has 'proxy' and logs print 'rest proxy enabled'; Gateway WebSocket and Telegram (same proxy) work, and curl to Discord API via the same proxy succeeds — suggests Discord REST client in OpenClaw 2026.2.25 (Node 25.6.1, macOS) is not honoring proxy configuration." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary clearly states the core bug (Discord REST requests failing with 'fetch failed' despite proxy enabled and other clients working) but includes minor noisy details (specific OpenClaw/Node/macOS version) that aren't needed for deduplication." - }, - "usage": { - "input_tokens": 841, - "output_tokens": 760 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "Discord REST API calls made by @buape/carbon's RequestClient ignore the channels.discord.proxy entry in openclaw.json — only the WebSocket gateway respects the proxy. The client calls globalThis.fetch without a proxy-aware undici dispatcher, so REST requests (and memorySearch/attachment downloads) bypass the configured HTTP proxy on Node 22.", - "solution_summary": "Modify @buape/carbon's REST client to accept an undici dispatcher or explicit proxy configuration and have OpenClaw pass channels.discord.proxy to it so REST calls use the same proxy as the gateway. Short-term workaround: preload a proxy agent (NODE_OPTIONS=--require=proxy-preload.cjs) to call setGlobalDispatcher(new EnvHttpProxyAgent()), but avoid --use-env-proxy which breaks WebSocket upgrades.", - "maintainer_signal_summary": "Blocking for proxy-required deployments: gateway works but REST/embeddings/media downloads bypass proxy. A preload workaround exists; do not use NODE_OPTIONS=--use-env-proxy because it causes Discord WebSocket disconnections. Related: issue #25232 (attachment downloads bypass proxy).", - "dedupe_summary": "@buape/carbon RequestClient invokes globalThis.fetch without supplying an undici dispatcher, causing Discord REST API calls to ignore openclaw.json channels.discord.proxy; Node 22's native fetch doesn't honor HTTP_PROXY/HTTPS_PROXY without --use-env-proxy (which in turn breaks WebSocket connections)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technically specific summary that identifies the root cause (RequestClient using globalThis.fetch without an undici dispatcher causing channels.discord.proxy to be ignored) and relevant Node 22 proxy behavior, so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 850, - "output_tokens": 876 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "OpenClaw shows performance degradation under sustained load with long-running conversations: unbounded context growth (writer ~42k tokens, reasoning ~90k tokens) increasing per-turn latency, Discord lane blocking causing serialization under load, and typing indicators stuck until TTL in some code paths.", - "solution_summary": "Applied configuration limits (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, agents.defaults.subagents.maxConcurrent 8→4, agents.defaults.subagents.maxChildrenPerAgent 8→4, agents.defaults.contextTokens 80000) and increased channels.discord.eventQueue.listenerTimeout 120000→300000; improved patching script to cover hash variants; restarted gateway and observed no new \"lane wait exceeded\" or \"typing TTL reached\" errors, though restart is not a full fix under sustained load. Recommendation: make limits adaptive or expose them in openclaw config.", - "maintainer_signal_summary": "Maintainers lowered concurrency limits, imposed a 80k token context cap, extended Discord event queue listener timeout, and updated openclaw-rootfix-v2026.3.11.sh to patch all reply/compact/pi-embedded/gateway-cli/register.agent hash variants; gateway restart validated immediate symptom reduction but long-term mitigation requires config tuning.", - "dedupe_summary": "Unbounded context token growth (writer ~42k, reasoning ~90k) causing rising latency, Discord lane blocking and stuck typing; mitigated by reducing concurrency (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents 8→4), setting agents.defaults.contextTokens=80000, and increasing channels.discord.eventQueue.listenerTimeout 120000→300000; script hardening in openclaw-rootfix-v2026.3.11.sh." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary captures the core technical problem (unbounded context growth causing latency, lane blocking, stuck typing) and remedial configuration changes, but includes minor noisy details (exact config values and a script/version name) that aren't necessary for deduplication." - }, - "usage": { - "input_tokens": 784, - "output_tokens": 830 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord threads repeatedly re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every conversation turn, causing context contamination, ballooning token usage, prompt-cache busting and performance degradation in long threads.", - "solution_summary": "A manual hotfix was applied to compiled dist files: dist/pi-embedded-D6PpOsxP.js prevents re-injection of the listed Discord thread fields, and dist/compact-1mmJ_KWL.js adds an isNewSession guard for thread starter and replaces resolveAgentMainSessionKey with buildAgentMainSessionKey(...); validation shows thread context no longer repeats and prompt caching is restored.", - "maintainer_signal_summary": "High risk: changes were made directly to compiled dist/ files and will be lost on update/reinstall. Recommended actions: implement the fix in source, add the isNewSession guard in the session handler, correct session key construction, and add post-update revalidation to catch regressions; refer to related issues #41355, #34228, #37047.", - "dedupe_summary": "Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) every turn, corrupting context and busting prompt cache; temporary fix applied in dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js to stop injection and add an isNewSession guard, but the change must be ported to source to persist." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the root cause (Discord thread metadata re-injected each turn causing context pollution and cache bust) and the temporary dist-file fix, with no template noise, so it will cluster well with similar issues." - }, - "usage": { - "input_tokens": 796, - "output_tokens": 711 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord thread messages have volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) re-injected on every turn, causing context pollution, performance degradation in long threads, and prompt cache busting with higher token costs.", - "solution_summary": "A manual patch was applied to the compiled file dist/pi-embedded-D6PpOsxP.js to stop re-injecting those Discord thread metadata fields after the first turn; validation shows repeated context stops, prompt caching is effective again, and billed tokens drop significantly. The recommended next step is to implement a source-level fix and keep a post-update revalidation script.", - "maintainer_signal_summary": "High regression risk: the fix was made directly in compiled dist files and will be lost on update/reinstall. Maintainters should port the patch into source, release an official fix, and consider automated revalidation after updates. Related issues: #41355, #34228, #37047.", - "dedupe_summary": "dist/pi-embedded-D6PpOsxP.js re-injects Discord thread volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, token bloat and prompt cache invalidation; manual dist patch restricts injection to the first turn." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that names the affected compiled file, the exact metadata re-injected, the impact (context pollution/token bloat/cache invalidation), and the intended fix scope (restrict to first turn), so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 804, - "output_tokens": 557 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "Two runtime regressions in Discord thread handling: per-turn reinjection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing unbounded context growth and performance/latency degradation, and the session resolver incorrectly falling back to agent:*:main instead of resolving live thread-bound sessions, causing CLI routing to the wrong session.", - "solution_summary": "Applied direct runtime patches to compiled bundles (dist/reply-BCcP6j4h.js, dist/compact-1mmJ_KWL.js, dist/pi-embedded-Cz5VjpnY.js, dist/gateway-cli-B-E8XzUM.js, dist/register.agent-jS8B5meo.js) to stop per-turn metadata reinjection and make resolver lookups consistent by delivery context (channel+to+accountId); added idempotent re-application script and backups, restarted and validated the gateway.", - "maintainer_signal_summary": "Patches are manual edits to compiled bundles and will be lost on updates—maintainers must run the provided re-application script and keep backups (*.bak-rootfix-*); gateway restart and log checks passed and a verification checklist and recommended smoke/long-thread tests are documented.", - "dedupe_summary": "Runtime patches to compiled dist JS bundles to remove per-turn reinjection of message_id/reply_to_id/timestamp/Thread starter/InboundHistory that caused context growth in long Discord threads, and to fix session resolver lookup to use delivery context (channel+to+accountId) preventing fallback to agent:*:main; includes idempotent reapply script and backup files." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise and specific about the two core fixes (removing per-turn metadata reinjection causing context growth and fixing session resolver to use delivery context), with only a minor operational detail about reapply scripts that isn't necessary for deduping." - }, - "usage": { - "input_tokens": 780, - "output_tokens": 708 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "OpenClaw 2026.3.2 fails to send Discord reply messages with \"TypeError: fetch failed\" even though incoming messages are received and logs indicate both REST and gateway proxies are enabled. The configured HTTP proxy (http://127.0.0.1:7890) works for curl, but sending replies from the bot always fails in systemd-run Node 22 environment.", - "solution_summary": "No patch provided in the report; reporter suggests the Discord REST client is not applying the configured proxy when sending replies. Likely fix: ensure the fetch/http client used for Discord REST reply requests uses the configured proxy agent or proxy environment variables so outbound requests go through the Clash proxy.", - "maintainer_signal_summary": "This is a regression (worked previously) and blocks outgoing replies while receives and model responses succeed; reproducible under systemd with Node 22 and OpenClaw 2026.3.2. Logs show 'discord: rest proxy enabled' and 'discord: gateway proxy enabled' yet final reply fails, indicating investigation into how fetch is invoked for reply sends is required.", - "dedupe_summary": "OpenClaw 2026.3.2: Discord replies fail with 'final reply failed: TypeError: fetch failed' despite 'discord: rest proxy enabled' and an http://127.0.0.1:7890 Clash proxy configured; incoming messages and model responses work and curl via the proxy succeeds, implying Discord REST reply requests are not using the configured proxy (regression with Node 22, systemd)." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary accurately states the core bug (Discord REST replies failing despite proxy config and incoming messages working) but includes minor noise (version/Node/systemd details) that aren't needed for deduplication." - }, - "usage": { - "input_tokens": 720, - "output_tokens": 705 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the CLI command `openclaw agent --channel discord --to ...` routes messages to the generic session `agent:*:main` instead of the live Discord channel/thread session because the compiled session resolver doesn't lookup by delivery context and Discord targets aren't normalized.", - "solution_summary": "A local manual patch was applied to dist/gateway-cli-BjsM6fWb.js to normalize Discord targets (prefixing with 'channel:'), add session lookup by delivery context (channel + to + accountId), and only fallback to agent:*:main if no matching session is found; this makes CLI routing hit the correct live session.", - "maintainer_signal_summary": "High regression risk: change was made directly in compiled dist/ file and will be lost on update; fix needs to be implemented in source, covered by tests, and coordinated with related issues (#36401, #41562) to avoid reverting behavior.", - "dedupe_summary": "CLI routing bug: session resolver in dist/gateway-cli-BjsM6fWb.js does not lookup by delivery context (channel+to+accountId) and fails to normalize Discord target IDs (e.g., '148...' vs 'channel:148...'), causing fallback to agent:*:main; patched by adding normalization and delivery-context session lookup." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the core bug (session resolver in dist/gateway-cli not using delivery-context lookup and failing to normalize Discord targets, causing fallback to agent:*:main) and would match similar routing/normalization issues." - }, - "usage": { - "input_tokens": 785, - "output_tokens": 613 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 a regression in the compiled file dist/compact-1mmJ_KWL.js calls a non-existent resolveAgentMainSessionKey(), causing ReferenceError at runtime and breaking the agent command and gateway processing.", - "solution_summary": "Manually patched dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...), restoring the session resolver and eliminating the ReferenceError; validation shows agent commands and new writer/reasoning sessions work correctly.", - "maintainer_signal_summary": "This is a high-risk regression introduced by a prior fix (fix #3) and fixed only in compiled dist files; maintainers must apply the correction in source (not just dist) before reapplying or releasing to avoid the ReferenceError being reintroduced.", - "dedupe_summary": "dist/compact-1mmJ_KWL.js calls undefined resolveAgentMainSessionKey instead of the available buildAgentMainSessionKey, causing ReferenceError and agent command failures; manual hotfix replaces the call in the compiled bundle but will be lost on reinstall/upstream updates." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary that identifies the exact faulty compiled file, the incorrect function call causing a ReferenceError, and that a local hotfix exists but will be overwritten, which is ideal for deduplication." - }, - "usage": { - "input_tokens": 756, - "output_tokens": 527 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "Discord plugin's REST API requests and health check ignore channels.discord.proxy (only WebSocket uses it), causing 'fetch failed' and message send timeouts in proxy-required environments despite curl via the proxy working.", - "solution_summary": "Patch the Discord channel to apply channels.discord.proxy to all HTTP/REST API calls and health checks (not just WebSocket), ensure the gateway passes the proxy to the HTTP/fetch client, add tests and release a fix.", - "maintainer_signal_summary": "Regression reported on OpenClaw 2026.3.8; labeled bug/regression. Reporter requests urgent fix so REST calls and health check respect channels.discord.proxy and message delivery works behind a proxy.", - "dedupe_summary": "Discord REST API requests and health check bypass channels.discord.proxy (only WebSocket uses the proxy), causing 'fetch failed' and message send timeouts in proxy-required environments." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that focuses on the core bug — channels.discord.proxy not applied to REST API/health checks (only WebSocket), causing fetch failures — with no extraneous template text." - }, - "usage": { - "input_tokens": 712, - "output_tokens": 594 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "En OpenClaw v2026.3.11 el session resolver del comando agent dentro de dist/compact-1mmJ_KWL.js no realiza lookup por delivery context (channel+to+accountId), por lo que resuelve sesiones a agent:*:main en vez de la sesión viva del canal/target; provoca inconsistencia entre routing del CLI gateway y el comando y respuestas mal asociadas a sesiones.", - "solution_summary": "Parche manual en dist/compact-1mmJ_KWL.js: se añadió lookup por delivery context (combinación channel + to + accountId) en el session resolver y se mantiene fallback a main solo si no se encuentra sesión específica; validado que el agent resuelve y asocia respuestas correctamente.", - "maintainer_signal_summary": "Parche aplicado directamente en archivos compilados en dist/ (high risk): será sobrescrito en actualizaciones/reinstalaciones; se requiere corrección en la fuente/build para persistir el arreglo y evitar regresiones en futuras releases.", - "dedupe_summary": "dist/compact-1mmJ_KWL.js (OpenClaw v2026.3.11) session resolver for agent command lacks delivery-context lookup (channel+to+accountId), causing fallback to agent:*:main and incorrect session association; manual compiled-file patch adds delivery-context lookup and fallback to main only if no specific session found." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Clearly and specifically states the core bug (compiled session resolver missing delivery-context lookup causing fallback to agent:*:main) and the resulting patch, giving strong, focused signal for deduplication." - }, - "usage": { - "input_tokens": 726, - "output_tokens": 472 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "The OpenClaw Control UI contains most interface text hardcoded in English (settings, buttons, agent/skill pages, channel configs, status messages), creating usability barriers for non-English users, particularly Chinese speakers.", - "solution_summary": "Extract hardcoded strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translated strings, implement a language selector or auto-detect, and build reusable infrastructure to add more languages.", - "maintainer_signal_summary": "Feature request marked Medium-High priority for UX accessibility; notes docs already have zh-CN translations and that the built UI bundle contains hardcoded strings; asks whether this is on the roadmap or open for contributors.", - "dedupe_summary": "Control UI hardcoded English strings; request to implement i18n starting with Simplified Chinese (zh-CN) by extracting strings to locale files, adding translations, and providing a language selector/auto-detect." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core feature request (implement i18n for Control UI, extract strings, add zh-CN translations, and language selector/auto-detect) with no boilerplate or extraneous details." - }, - "usage": { - "input_tokens": 647, - "output_tokens": 395 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "WhatsApp group chats couldn't apply channel-level systemPrompt settings at account or group granularity, preventing per-account and per-group prompt customization (including wildcard defaults) and causing unclear override semantics between root and account group maps.", - "solution_summary": "Adds systemPrompt support to WhatsApp config schema and resolution logic, supporting channels.whatsapp.systemPrompt, channels.whatsapp.accounts..systemPrompt, channels.whatsapp.groups..systemPrompt and account-level groups with wildcard '*'; implements account-over-root precedence, group-specific then wildcard fallback, account groups fully replace root groups, plus tests and docs.", - "maintainer_signal_summary": "Closes issue #7011 and replaces the author's prior larger PR; backward compatible with no security/network changes, limited scope to WhatsApp gateway/orchestration and integration code, includes tests/docs, and does not alter hot-reload or non-WhatsApp behavior (size M).", - "dedupe_summary": "Introduce WhatsApp systemPrompt hierarchy: channels.whatsapp.systemPrompt fallback, overridden by channels.whatsapp.accounts..systemPrompt; resolve group prompt from accounts..groups. then '*' wildcard; account 'groups' map fully replaces root channels.whatsapp.groups (no merge); final injected prompt = account prompt + resolved group prompt joined with a blank line." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the new WhatsApp systemPrompt resolution hierarchy, wildcard/group semantics, replacement behavior, and final prompt composition—no boilerplate or irrelevant details." - }, - "usage": { - "input_tokens": 2423, - "output_tokens": 889 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before agent processing, and group policy resolution omitted account-scoped data (no accountId), causing account-scoped groupAllowFrom/allowFrom and allowlist fallback to be ignored.", - "solution_summary": "Enable append processing only for WhatsApp group messages (DM append history still skipped) and propagate accountId when resolving WhatsApp group policy so account-scoped channels.whatsapp.accounts. and its groupAllowFrom/allowFrom are considered for gating and allowlist fallback.", - "maintainer_signal_summary": "Changes touch src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts; no config/schema/dist changes, no debug logging added. Targeted tests and live traces verify groups now route and gate correctly; revert by restoring the two files if needed. Risks: processing backlog/history for groups may change behavior, but DM append remains skipped and tests cover account-scoped allowlist behavior.", - "dedupe_summary": "WhatsApp group append events were skipped before enqueueing and group policy helpers resolved without accountId, causing account-scoped channels.whatsapp.accounts. groupAllowFrom/allowFrom fallback to be ignored; fix scopes append handling to groups and uses account-scoped account for allowlist fallback." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core bug and the fix: append handling for WhatsApp groups was skipped and policy resolution lacked account-scoping, now both are corrected to use group-scoped append processing and account-scoped allowlist fallback." - }, - "usage": { - "input_tokens": 1975, - "output_tokens": 716 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Canvas (A2UI) returned 401 when accessed through Tailscale Serve because resolveClientIp() returned undefined for requests from a trusted proxy with no forwarding headers, and isLocalDirectRequest() did not fall back to the socket address when allowRealIpFallback was enabled and XFF resolved to a non-loopback Tailscale IP.", - "solution_summary": "Fixes return the socket address from resolveClientIp() when no forwarding headers are present, make isLocalDirectRequest() check req.socket.remoteAddress when allowRealIpFallback is enabled, gate .ts.net host acceptance behind an existing auth.allowTailscale flag, thread allowTailscale through callers, and update docs and tests to reflect required gateway.trustedProxies and allowRealIpFallback settings.", - "maintainer_signal_summary": "Includes unit tests (auth/net), docs changes, and depends on PR #50636 (merge first); behavior is backward-compatible since allowTailscale defaults to true for tailscale.mode = \"serve\", and reviewers should inspect auth/net and server-http related files for the socket-fallback and .ts.net gating logic.", - "dedupe_summary": "Canvas returns 401 behind Tailscale Serve because resolveClientIp returned undefined for direct trusted-proxy requests without X-Forwarded headers and isLocalDirectRequest didn’t fallback to req.socket.remoteAddress when allowRealIpFallback is true; fix adds socket-address fallback, gates .ts.net host-header acceptance behind auth.allowTailscale, and threads allowTailscale through callers." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the root causes (resolveClientIp and isLocalDirectRequest fallbacks), the concrete fixes (socket fallback, .ts.net gating, threading allowTailscale), and uses precise technical terms useful for deduplication." - }, - "usage": { - "input_tokens": 1814, - "output_tokens": 676 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "checkInboundAccessControl() computed an isSelfChat heuristic but never used it as a guard, so when WhatsApp channel was configured without an explicit groupPolicy non-self messages (groups, DMs from strangers, broadcasts) could pass through because resolveWhatsAppRuntimeGroupPolicy() defaulted to \"open\". This violated self-chat mode's isolation intent by allowing non-self messages to reach downstream processing.", - "solution_summary": "Added an early-return guard in checkInboundAccessControl() that immediately rejects any non-self message when account.selfChatMode === true, placed before group/DM policy evaluation. The change intentionally checks only the explicit selfChatMode flag (not the heuristic isSelfChat) to avoid blocking teammates allowed via allowFrom; isSelfChat is still returned for UI/logging.", - "maintainer_signal_summary": "All existing access-control tests (9) pass; code-path analysis and targeted verifications were done and the guard placement is intentional. Change affects only WhatsApp inbound access-control (src/web/inbound/access-control.ts) and can be reverted by removing the early-return block; note that auto-detected self-chat (heuristic-only) remains unblocked by design.", - "dedupe_summary": "WhatsApp access-control bug: explicit selfChatMode=true did not block non-self messages because isSelfChat was computed but unused and groupPolicy defaulted to \"open\", allowing groups/strangers through; fix adds an early-return guard rejecting non-self messages when account.selfChatMode === true while preserving teammate allowFrom behavior." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that states the core WhatsApp self-chat access-control bug, root cause (unused isSelfChat + groupPolicy default), and the precise fix (early-return guard on account.selfChatMode) without boilerplate." - }, - "usage": { - "input_tokens": 1529, - "output_tokens": 678 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "With groupPolicy set to \"allowlist\", group messages from senders not in groupAllowFrom are dropped at access-control (allowed: false → continue) so the agent never sees them in the \"Chat messages since your last reply\" context block, breaking the documented flow where unmentioned messages should be stored for context.", - "solution_summary": "Adds an opt-in config flag groupContextFromAll (top-level and per-account, default false) that makes access-control return storeForContext for non-allowlisted senders, marks those inbound messages as contextOnly (no read receipts), and records them as pending group history so the agent sees them for context but they cannot trigger replies.", - "maintainer_signal_summary": "Change is opt-in and backwards-compatible (default false), small scope touching access-control, monitor, types, config schema and account resolution; labeled channel: whatsapp-web, size: S, stale. Greptile flagged a 3/5 confidence due to a potential allowlist normalization/matching edge case and the new path bypassing some group gating/state updates.", - "dedupe_summary": "WhatsApp allowlist behavior drops non-allowlisted group messages entirely; this PR introduces groupContextFromAll to store those messages as pending context (contextOnly) without triggering replies, threads a new access-control outcome (storeForContext) through inbound monitor, skips read receipts, and persists entries in group history (per-account or top-level config, default false)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary of the core bug (allowlist discards group messages) and the concrete fix (new groupContextFromAll config, storeForContext outcome, context-only storage, skip receipts, persist history) with specific terms that will match duplicates." - }, - "usage": { - "input_tokens": 1546, - "output_tokens": 786 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "sessions_spawn always created a new random-UUID session for sub-agents, preventing deterministic session reuse and preserving conversation history across spawns; there was also a Greptile finding about a session key bypassing an allowlist.", - "solution_summary": "Adds an optional sessionKey parameter: short keys map to agent:{agentId}:subagent:{key}, fully-qualified keys are accepted after validating the embedded agentId matches targetAgentId, and omission falls back to crypto.randomUUID(); includes four tests for deterministic key, UUID fallback, fully-qualified passthrough, and cross-agent mismatch rejection.", - "maintainer_signal_summary": "Reviewer flagged three blocking issues: a test imports a non-existent module (openclaw-tools.js vs clawdbot-tools.js) causing compilation failure, a case-sensitivity bug in fully-qualified agentId validation (embedded ID not normalized), and inconsistent error handling (throw vs returning jsonResult), lowering confidence for merge.", - "dedupe_summary": "Introduce optional sessionKey to sessions_spawn to reuse sub-agent sessions: short keys namespace to agent:{agentId}:subagent:{key}, fully-qualified keys validated to prevent cross-agent session injection, fallback to crypto.randomUUID(); tests added; problems found—broken test import, case-sensitive agentId mismatch bug, and inconsistent error handling." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "The summary clearly describes the new optional sessionKey behavior, namespacing, validation, and fallback which are the core signals for deduplication, but it includes extra noise about broken tests and minor bugs that aren't needed for clustering." - }, - "usage": { - "input_tokens": 1599, - "output_tokens": 628 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) used editMessageText and caused Telegram to return 400 \"there is no text in the message to edit\", so message(action=edit) on captionable messages failed and left the UI inconsistent.", - "solution_summary": "editMessageTelegram now detects the specific 400 error via NO_TEXT_IN_MESSAGE_RE and retries with editMessageCaption for media messages; buttons-only updates call editMessageReplyMarkup directly; MESSAGE_NOT_MODIFIED is treated as success and HTML parse fallback preserved. Six unit tests were added covering retry, buttons-only path, error propagation, and MESSAGE_NOT_MODIFIED handling.", - "maintainer_signal_summary": "Backward-compatible change limited to src/telegram/send.ts, no public API changes; introduces calls to editMessageCaption and editMessageReplyMarkup and a regex to match Telegram's \"no text\" error; includes tests and can be reverted by restoring the previous send.ts. Risk: regex may fail if Telegram changes error wording—unmatched errors will continue to propagate as before.", - "dedupe_summary": "Telegram editMessageText fails for captionable media (photo, video, audio, document, animation) with 400 'there is no text in the message to edit'; fix adds NO_TEXT_IN_MESSAGE_RE to catch that 400 and retry with editMessageCaption (and uses editMessageReplyMarkup for button-only edits), plus tests covering the retry and MESSAGE_NOT_MODIFIED handling." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Clear, concise technical summary naming the exact failure (editMessageText 400 for captionable media) and the precise fix (catch specific 400, retry with editMessageCaption; use editMessageReplyMarkup for button-only edits), so it will cluster correctly with similar Telegram caption-edit issues." - }, - "usage": { - "input_tokens": 1480, - "output_tokens": 598 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "PR #24962 hardcoded disableBlockStreaming: true for WhatsApp to prevent reasoning leaks, which also inadvertently disabled block streaming entirely so messages were batched and sent only when the agent finished, ignoring per-account or global blockStreaming settings.", - "solution_summary": "Restore config-driven behavior: deliver callback now only suppresses tool payloads (not all non-final payloads) and disableBlockStreaming is derived from channels.whatsapp.blockStreaming falling back to agents.defaults.blockStreamingDefault; upstream shouldSuppressReasoningPayload continues to filter reasoning content.", - "maintainer_signal_summary": "All inbound contract tests pass and live WhatsApp verification shows progressive delivery at text_end when configured; reasoning/tool payload suppression remains intact and defaults unchanged; reviewers should watch for any regression in shouldSuppressReasoningPayload.", - "dedupe_summary": "WhatsApp block streaming was disabled by a hardcoded disableBlockStreaming:true in PR #24962; this PR makes block streaming respect per-account channels.whatsapp.blockStreaming and agents.defaults.blockStreamingDefault and narrows suppression in the deliver callback to tool payloads while relying on shouldSuppressReasoningPayload upstream." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core change—removing the hardcoded disable, restoring per-account/global config-driven WhatsApp block streaming, and narrowing suppression to tool payloads while noting upstream reasoning filtering." - }, - "usage": { - "input_tokens": 1472, - "output_tokens": 446 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Sending videos above the old 5MB media limit caused the Telegram gateway to hang indefinitely because undici's Agent.close() deadlocked when the HTTP response body was left unconsumed, making the entire Telegram channel unresponsive until restart.", - "solution_summary": "Raised default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher to avoid Agent.close deadlock (fallback to destroy after a 3s timeout), added timeouts and retry logic to getFile and media downloads, made media download failures non-fatal so captions/text still forward, and extended UI metadata stripping; tests updated.", - "maintainer_signal_summary": "Default media limit changed (5MB→20MB) but remains configurable via channels.telegram.mediaMaxMb; key files touched: src/infra/net/ssrf.ts and src/telegram/bot.ts; monitor closeDispatcher 3s timeout logs and media download timeouts, and revert limit via config or revert commits if needed.", - "dedupe_summary": "Telegram gateway hung on video messages when unconsumed response bodies caused undici Agent.close deadlock; fix increases mediaMaxMb to 20, adds timeouts/retries to getFile/fetchRemoteMedia, makes download errors non-fatal, and implements a closeDispatcher fallback (destroy after 3s)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core bug (undici Agent.close deadlock on unconsumed response bodies causing gateway hangs) and the key fixes (increase mediaMaxMb, timeouts/retries, non-fatal download errors, closeDispatcher fallback), with no boilerplate noise." - }, - "usage": { - "input_tokens": 1568, - "output_tokens": 626 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "Telegram inbound body fallback always used the hardcoded placeholder \"\" for non-audio media when no caption/text was present, causing documents (PDF/DOCX) to be misidentified to the agent as images and leading to incorrect agent responses.", - "solution_summary": "Changed resolveTelegramInboundBody to use the already-computed, type-aware placeholder variable (e.g., , ) instead of hardcoded , and added unit tests in extensions/telegram/src/bot/helpers.test.ts and extensions/telegram/src/bot.create-telegram-bot.test.ts to cover document and other media types; media download and extraction logic unchanged.", - "maintainer_signal_summary": "PR closes #7116, scoped bugfix to agent-facing placeholder only (no changes to media download/processing), includes unit tests that validate placeholder text and message handling, and notes a small risk that downstream code pattern-matching on could be affected.", - "dedupe_summary": "resolveTelegramInboundBody hardcoded \"\" for non-audio inbound Telegram media, mislabeling documents as images; fix replaces the hardcoded string with the type-aware placeholder so document messages produce \"\", plus unit tests to lock behavior." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary identifying the function, the mislabeling bug, and the fix (type-aware placeholder) with minimal noise, so it will cluster with similar Telegram media placeholder issues." - }, - "usage": { - "input_tokens": 1395, - "output_tokens": 544 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "MEDIA_MAX_BYTES (5 MB) was hardcoded in src/media/store.ts so users could not override it, causing files larger than 5 MB to silently fail to stage into sandboxed agents and preventing processing of common large files (PDFs, presentations, high-res images).", - "solution_summary": "Adds new media.maxBytes config field (integer, optional, default 5 MB, max 500 MB) and replaces the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(); the gateway reads the config at boot, error messages report the configured limit, and tests were updated and pass.", - "maintainer_signal_summary": "All 56 tests pass and bot review threads were addressed; reviewers should verify backward compatibility (deprecated exported constant retained), schema enforcement of the 500 MB cap, and that per-channel download limits remain untouched; beware disk-usage risk if users set very large limits.", - "dedupe_summary": "Make MEDIA_MAX_BYTES configurable via media.maxBytes (default 5 MB, capped at 500 MB), replacing hardcoded 5MB in src/media/store.ts with getMediaMaxBytes()/setMediaMaxBytes(), applied at gateway boot, and update error messages to show the configured MB value; tests updated and passing." - }, - "judge": { - "score": 4, - "has_boilerplate": true, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise and specific about the core change (making MEDIA_MAX_BYTES configurable, where it's applied, replacement of hardcoded constant, default and cap), but includes a minor testing note that is unnecessary for deduplication." - }, - "usage": { - "input_tokens": 1516, - "output_tokens": 873 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "When Telegram media download fails (due to exceeding mediaMaxMb default 5MB or Telegram Bot API getFile 20MB limit, or other API errors), the handler returned early and silently dropped the entire message so the agent received no ack, no visibility, and the user got no useful feedback.", - "solution_summary": "On download failure, extract file metadata (name, size, type, dimensions) from the Telegram message object and inject a metadata stub into msg.text describing the file and why it couldn't be downloaded (e.g. exceeds limit or download failed), then let the message continue through the normal pipeline so the agent can handle/respond.", - "maintainer_signal_summary": "This is a bug fix to the Telegram integration (src/telegram/bot-handlers.ts) and is backward-compatible; previous reply-to-user notifications were removed so the agent decides responses. Verified by code/path inspection (not live runtime), safe to revert the single commit if needed, and the change only mutates msg.text in error paths previously dropping messages.", - "dedupe_summary": "Telegram handler previously returned early on media download failures and dropped messages; now it builds a file metadata stub from the Telegram message object and injects it into msg.text so files exceeding mediaMaxMb (default 5MB) or Telegram getFile limits (20MB) and other download errors are forwarded to the agent with an explanatory warning." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that clearly states the bug (Telegram handler dropped messages on media download failure), the implemented fix (builds file metadata stub and injects it into msg.text), and the relevant conditions (mediaMaxMb and Telegram getFile limits), so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 1358, - "output_tokens": 660 - } - }, - { - "thread_id": 5796, - "number": 24117, - "kind": "pull_request", - "title": "feat(cli): add --session-key targeting for openclaw agent", - "summary": { - "problem_summary": "The openclaw agent CLI lacked a way to target stable session keys, forcing callers to rely on rotating sessionId UUIDs, sender-derived --to, or falling back to the agent main key, causing callback routing ambiguity for automation flows.", - "solution_summary": "Adds a new --session-key flag to openclaw agent, surfaces sessionKey through AgentCliOpts into resolveSessionKeyForRequest, maintains precedence (sessionKey > sessionId > --to > --agent), updates help/validation text, and adds tests for CLI parsing and gateway forwarding.", - "maintainer_signal_summary": "Small, low-risk change (label size: S) with tests passing and a stated confidence score of 5/5; changes touch register.agent.ts, agent-via-gateway.ts, AgentCliOpts, and corresponding tests; backward-compatible and opt-in.", - "dedupe_summary": "Introduce --session-key to openclaw agent CLI and forward sessionKey into resolveSessionKeyForRequest so callers can target stable session keys (e.g., agent:demo:slack:channel:c0123456789) instead of UUID sessionId; updates AgentCliOpts, register.agent.ts, agent-via-gateway.ts, help text, and adds unit tests." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Succinctly describes the new --session-key CLI flag, its purpose (target stable session keys vs UUID sessionId), and the key code/files changed, making it a strong signal for deduplication." - }, - "usage": { - "input_tokens": 1330, - "output_tokens": 512 - } - }, - { - "thread_id": 766, - "number": 11611, - "kind": "pull_request", - "title": "feat: separate group-level allowlist from sender-level command authorization", - "summary": { - "problem_summary": "When channels.whatsapp is configured with groupPolicy: \"allowlist\" the current implementation uses groupAllowFrom (sender phone numbers) to gate all group interactions, preventing admins from allowlisting specific group JIDs, allowing anyone to chat in approved groups, or keeping slash commands restricted to owners while opening group chat to all participants.", - "solution_summary": "If groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access control now gates by group JID membership in that mapping (only listed groups accepted) while leaving groupAllowFrom to authorize slash commands; if no groups mapping is present, the prior sender-based allowlist behavior is preserved.", - "maintainer_signal_summary": "Code changes touch src/web/inbound/access-control.ts (integrating resolveChannelGroupPolicy), update types JSDoc, and add a Vitest suite with four tests covering allowlist-by-group, blocking unapproved groups, legacy fallback, and command authorization separation; depends on PR #3326 and notes one stale inline comment to fix. Tests cover new branches and the change is described as localized and safe to merge.", - "dedupe_summary": "WhatsApp: change groupPolicy: \"allowlist\" behavior so that when channels.whatsapp.groups mapping exists inbound group access is gated by group JID membership (allow any sender in approved groups), while groupAllowFrom remains the gate for slash commands; falls back to sender-based filtering when no groups mapping is present." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Accurately and concisely describes the core behavioral change (group JID-based gating when groups mapping exists, sender-based fallback, and command authorization via groupAllowFrom) using specific config terms relevant for clustering." - }, - "usage": { - "input_tokens": 1380, - "output_tokens": 784 - } - }, - { - "thread_id": 17924, - "number": 46425, - "kind": "pull_request", - "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", - "summary": { - "problem_summary": "Configured Telegram mediaMaxMb is ignored when delivering final reply messages containing media; loadWebMedia() in the reply delivery path always falls back to the default 100 MB cap so lower account limits aren’t enforced.", - "solution_summary": "Threaded mediaMaxBytes through the Telegram outbound reply chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia receives the configured maxBytes for replies; added unit tests to verify behavior and left direct-send logic unchanged.", - "maintainer_signal_summary": "Two new unit tests added and all related test suites pass (108 tests across delivery, dispatch, native-commands, bot-message); change is small, backward-compatible, author addressed review comments and marked the PR ready with minimal risk.", - "dedupe_summary": "Outbound Telegram replies ignored telegramAccounts[].mediaMaxMb (loadWebMedia fell back to 100 MB); this PR threads mediaMaxBytes through bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts and bot-native-commands.ts so replies enforce the configured media size limit, with unit tests verifying maxBytes is passed or omitted." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the bug and the code paths changed (including filenames and config key) that would match duplicates, but it includes a minor testing-detail noise about unit tests that isn't necessary for deduplication." - }, - "usage": { - "input_tokens": 1381, - "output_tokens": 811 - } - }, - { - "thread_id": 5712, - "number": 37050, - "kind": "pull_request", - "title": "fix(telegram): add HTML parse fallback for media captions", - "summary": { - "problem_summary": "Sending media messages (photo, document, video, animation, audio, voice, video note) failed with a 400 \"can't parse entities\" error when captions contained invalid HTML entities because the media send path lacked the HTML-parse fallback that text messages use.", - "solution_summary": "Wrapped the media send callback in withTelegramHtmlParseFallback so on an HTML parse error the send is retried without parse_mode and with the raw caption; applied to all media types and covered by a new unit test that simulates a parse error then a successful retry.", - "maintainer_signal_summary": "Added a unit test 'retries media caption as plain text on HTML parse error'; all 52 tests pass. Change is backward-compatible, limited to send.ts media send callback, and can be reverted by restoring that function; risk is only visual differences in caption formatting when fallback occurs.", - "dedupe_summary": "Telegram media send (sendPhoto/sendDocument/sendVideo/sendAnimation/sendAudio/sendVoice/video note) had no HTML-parse fallback, causing 400 parse-entity errors for captions with invalid HTML; this PR adds withTelegramHtmlParseFallback to retry without parse_mode and send the raw caption." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the bug (media captions lacked HTML-parse fallback causing 400 errors) and the fix (wrap sends with withTelegramHtmlParseFallback to retry without parse_mode), with no boilerplate or noise." - }, - "usage": { - "input_tokens": 1255, - "output_tokens": 492 - } - }, - { - "thread_id": 21769, - "number": 55280, - "kind": "pull_request", - "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", - "summary": { - "problem_summary": "WhatsApp had block streaming effectively disabled (disableBlockStreaming hardcoded true), causing intermediate text blocks to be batched and sometimes resulting in a delivery race where neither blocks nor the final reply reached the user. A ternary bug also caused configured blockStreaming to be ignored (undefined vs false).", - "solution_summary": "Introduce channels.whatsapp.blockStreaming (per-account/global), add sensible coalescing defaults, and change the dispatcher to sendBlockReplyAsync which awaits confirmed delivery before marking sentContentKeys. Update process-message to read the account config and fix the ternary, update dispatch to await async block delivery, and add/update tests for both streaming paths.", - "maintainer_signal_summary": "Tests updated and new cases added; manual verification shows streaming off yields a single final reply and streaming on delivers intermediate blocks; PR targets root-cause fix of the silent-turn bug rather than the prior revert workaround. Labels: channel: whatsapp-web, channel: feishu, size: S.", - "dedupe_summary": "Block streaming was effectively disabled for WhatsApp via a hardcoded disableBlockStreaming=true and a ternary bug ignored configured blockStreaming, causing a race: optimistic enqueue of block replies marked sentContentKeys before actual delivery, suppressing final replies; fix adds channels.whatsapp.blockStreaming config, blockStreamingCoalesceDefaults, and sendBlockReplyAsync which awaits delivery before marking sentContentKeys." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the root cause (hardcoded disableBlockStreaming and ternary bug), the delivery race (sentContentKeys set before delivery), and the concrete fixes (config option, coalescing defaults, and awaiting delivery via sendBlockReplyAsync)." - }, - "usage": { - "input_tokens": 1168, - "output_tokens": 588 - } - }, - { - "thread_id": 8098, - "number": 25543, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked a group-level allowlist: only sender-level groupAllowFrom was supported, so specific Signal groups couldn't be explicitly allowed or authorized to run control commands independent of sender checks.", - "solution_summary": "Adds channels.signal.groups. config plus SignalGroupConfig type and Zod schema, wires resolveChannelGroupPolicy into the Signal event handler to bypass sender-level groupAllowFrom for listed groups, and integrates group-level allow into command authorizers; docs updated with examples.", - "maintainer_signal_summary": "Addresses Greptile review by incorporating groupExplicitlyAllowed into commandGate authorizers, preserves hasGroupAllowFrom backwards-compat behavior, passes TypeScript checks and was manually tested; safe to merge with minimal risk.", - "dedupe_summary": "Introduce channels.signal.groups. allowlist that bypasses sender-level groupAllowFrom and authorizes listed Signal groups for message delivery and control commands by wiring resolveChannelGroupPolicy into the Signal event handler and adding SignalGroupConfig and Zod schema." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core change—adds a Signal group-level allowlist that bypasses sender checks, integrates with the event handler for delivery/command authorization, and adds config/types/schema—no boilerplate or irrelevant details." - }, - "usage": { - "input_tokens": 1110, - "output_tokens": 397 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel had no group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" with group IDs in groupAllowFrom, group messages were silently dropped because the sender-level filter compared sender IDs to group IDs and always failed.", - "solution_summary": "Added a groups config to SignalAccountSchemaBase (requireMention, tools, toolsBySender) and updated Signal's event handler to call resolveChannelGroupPolicy for group-level checks before falling back to sender-level groupAllowFrom; added four tests and verified full test suite passes.", - "maintainer_signal_summary": "Fixes #25540; small Signal-channel change with 4 new tests; all 624 config tests, 6 Signal monitor tests, and 17 group-policy tests pass. Labeled channel: signal, size: S. PR was AI-assisted and reviewed by @JamesPeck.", - "dedupe_summary": "Signal lacked group-level allowlisting (channels.signal.groups), so groupPolicy:\"allowlist\" with group IDs in groupAllowFrom failed because code compared sender phone/UUID to group IDs; fix adds groups schema and calls resolveChannelGroupPolicy in Signal access checks." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the bug and fix: missing group-level allowlist in Signal caused sender-vs-group ID mismatch and was resolved by adding groups schema and using resolveChannelGroupPolicy, which is ideal for deduplication." - }, - "usage": { - "input_tokens": 910, - "output_tokens": 513 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "WhatsApp messages were being coalesced into a single final payload because process-message.ts had disableBlockStreaming hardcoded true, so intermediate agent blocks (tool calls/sleeps) were not delivered as separate WhatsApp messages.", - "solution_summary": "Make WhatsApp respect the channels.whatsapp.blockStreaming config by adding resolveWhatsAppBlockStreamingEnabled(), replacing the hardcoded flag, registering blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in dock.ts, and fixing delivery logic so intermediate blocks count as delivered. Tests updated to cover enabled/default/disabled behaviors.", - "maintainer_signal_summary": "Change is backwards-compatible by defaulting to the previous behavior when blockStreaming is omitted/false, aligns WhatsApp with Telegram/Line behavior, includes a small test update (process-message.inbound-contract.test.ts) and capability registration in dock.ts; labeled channel: whatsapp-web and size: S.", - "dedupe_summary": "process-message.ts hardcoded disableBlockStreaming:true causing WhatsApp to batch intermediate blocks; PR switches to config-driven channels.whatsapp.blockStreaming via resolveWhatsAppBlockStreamingEnabled(), fixes queuedFinal-only delivery counting so intermediate payloads mark replies as delivered, registers blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in dock.ts, and updates inbound contract tests." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core change (removing hardcoded disableBlockStreaming for WhatsApp, making it config-driven, fixing delivery-count logic, and registering capability) with relevant file names and constants, no boilerplate." - }, - "usage": { - "input_tokens": 660, - "output_tokens": 555 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v7-two-pass.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v7-two-pass.json deleted file mode 100644 index 164be15..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v7-two-pass.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v7-two-pass", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "You will summarize a GitHub issue or pull request in two mental passes:\n\nPASS 1 — STRIP: Mentally remove all of the following before summarizing:\n- Template sections: headers like \"### Bug type\", \"### Steps to reproduce\", \"### Checklist\", \"## Testing\"\n- Checkboxes: \"- [x] Tests added\", \"- [ ] Docs updated\"\n- Environment details: OS versions, browser versions, Node versions\n- Reproduction steps: numbered step lists\n- Testing instructions: \"Run `npm test`\", \"Deploy to staging\"\n- Cross-references: \"Fixes #1234\", \"Related to #5678\"\n- Formatting: horizontal rules, badges, images unless they show the bug\n\nPASS 2 — EXTRACT: From what remains, identify:\n- What specific component or API is affected?\n- What exactly goes wrong or what change is being made?\n- What is the user-facing impact?\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nAll fields: plain text, no markdown, 1-3 sentences.\ndedupe_summary: the essential topic that would match other reports of the same issue. Be specific enough to distinguish from similar-but-different issues.", - "timestamp": "2026-03-30T17:57:44.188Z", - "aggregate": { - "avg_score": 4.93, - "avg_signal_density": 4.93, - "boilerplate_count": 1, - "boilerplate_pct": 3, - "would_cluster_correctly_pct": 100, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 48503, - "total_output_tokens": 27223 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "channels.discord.proxy only applies to the Discord WebSocket gateway; startup REST API calls (deploy commands, fetch bot identity) and the guild resolve path do not use the configured HTTP proxy and time out, leaving guild channels unresolved and the bot non-responsive in guilds when a proxy is required.", - "solution_summary": "Apply the configured proxy to undici's dispatcher for all Discord REST calls (including guild resolve) during provider startup or add a global proxy config that sets a ProxyAgent/setGlobalDispatcher; ensure guild resolution uses the proxied HTTP path and add retry logic.", - "maintainer_signal_summary": "Regression: previously worked but now fails in proxied environments; reproducible when an HTTP proxy is required. Logs show WebSocket proxied but REST calls fail with \"fetch failed\" and \"channels unresolved\"; a manual undici ProxyAgent bootstrap fixes REST but not guild resolve, indicating a separate code path. High priority because guild channels are unusable behind proxies.", - "dedupe_summary": "Discord REST API requests and guild resolution bypass channels.discord.proxy, causing REST timeouts and unresolved guild channels when an HTTP proxy is required." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific description of the bug (REST calls/guild resolution bypassing channels.discord.proxy and causing timeouts/unresolved channels) with no boilerplate, so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 2348, - "output_tokens": 724 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "Discord outbound REST sends (message sending) fail when channels.discord.proxy is set because Carbon's RequestClient uses fetch without proxy/agent support. Gateway/WebSocket connections work via the proxy, but REST calls error with \"fetch failed\", preventing bots from sending messages in proxy-required networks.", - "solution_summary": "Make the Carbon RequestClient honor channels.discord.proxy by wiring a proxy-capable HTTP/SOCKS agent into fetch (or using a proxy-aware HTTP client), ensuring all Discord REST endpoints, attachments, and media downloads route through the configured proxy; add tests and config documentation.", - "maintainer_signal_summary": "High-priority for users in proxy-restricted regions (tested in China); reproduction provided (channels.discord.proxy: \"http://127.0.0.1:7890/\") and logs show \"rest proxy enabled\" but fetch ignores the proxy. Fix requires adding proxy support to RequestClient/fetch or using env-based proxying.", - "dedupe_summary": "channels.discord.proxy not applied to Discord REST API (message send) because Carbon RequestClient/fetch lacks proxy support, causing send failures in proxy-required environments." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary stating that channels.discord.proxy isn't applied to REST sends because the Carbon RequestClient/fetch lacks proxy support, which directly explains the failure in proxy-required environments." - }, - "usage": { - "input_tokens": 1030, - "output_tokens": 746 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "When OpenClaw's Discord provider is configured with an HTTP proxy, the gateway/WebSocket login succeeds but some startup REST calls—specifically native command deployment (PUT /applications/.../commands) and bot identity fetch—consistently fail with \"fetch failed\", leaving the provider half-functional and triggering periodic stale-socket restarts.", - "solution_summary": "Ensure all Discord REST requests use the same proxy-aware fetch/dispatcher as the gateway/login by unifying transport code paths; apply proxy handling to native command deployment and bot identity fetch, add logging/tests to verify proxy usage and prevent mixed proxy/direct requests.", - "maintainer_signal_summary": "Investigate and reproduce with the reporter's proxy config, inspect differences between gateway and REST request code paths, patch the REST request implementation to use the proxy-aware dispatcher, add unit/integration tests and improved logging, and request additional logs/config if needed.", - "dedupe_summary": "Discord provider startup REST requests (native command deployment and bot identity fetch) fail with \"fetch failed\" behind an HTTP proxy while gateway login succeeds." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it names the failing Discord provider REST calls (native command deployment and bot identity fetch), the error ('fetch failed'), and that it occurs behind an HTTP proxy while gateway login succeeds, with no boilerplate noise." - }, - "usage": { - "input_tokens": 1176, - "output_tokens": 818 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "OpenClaw's Discord channel integration (discord.js REST client/gateway) logs that an HTTP proxy is enabled but HTTP REST calls to Discord fail with \"fetch failed\" and the WebSocket gateway never establishes; as a result the bot cannot deploy commands or fetch identity and the Discord channel is unusable.", - "solution_summary": "Ensure the configured HTTP proxy is actually passed to the underlying discord.js HTTP/fetch implementation and gateway connection (e.g., attach a proxy agent or configure fetch/global agent), so REST requests and the gateway use the proxy and succeed under WSL2/HTTP-proxy environments.", - "maintainer_signal_summary": "Config at ~/.openclaw/openclaw.json shows channels.discord.proxy and OpenClaw logs indicate proxy enabled, while curl through the same proxy succeeds; errors shown: deploy-rest:put:error ... error=fetch failed and failed to fetch bot identity; reproducible in WSL2, high severity, workaround is TUN-mode or alternate channel.", - "dedupe_summary": "Discord channel proxy config logged as enabled but discord.js REST/gateway calls fail with \"fetch failed\" under HTTP proxy (WSL2), causing Discord integration to be nonfunctional." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states that the Discord proxy is detected but discord.js REST/gateway calls fail with 'fetch failed' (WSL2), which accurately captures the core issue for deduplication." - }, - "usage": { - "input_tokens": 1013, - "output_tokens": 614 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "Discord gateway's REST API calls (sending messages) ignore the configured proxy (channels.discord.proxy and HTTP(S)_PROXY), causing 'TypeError: fetch failed' while the Discord WebSocket connection and curl via the proxy work correctly.", - "solution_summary": "Make Discord REST HTTP requests go through the same proxy-aware SSRF guard/fetch path used for web_search/web_fetch so they honor channels.discord.proxy and environment proxy variables.", - "maintainer_signal_summary": "Reproducible by configuring channels.discord.proxy or HTTP(S)_PROXY and sending a message; logs show 'discord final reply failed: TypeError: fetch failed'. The 2026.2.26 fix added a proxy-aware path for web tools but omitted Discord REST; Telegram and Discord WebSocket already honor the proxy.", - "dedupe_summary": "Discord REST API (message send) ignores configured proxy settings and fails with 'fetch failed' while WebSocket and other channels use the proxy correctly." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (Discord REST message sends ignore proxy and error with 'fetch failed', while WebSocket works), with no template noise and good keywords for clustering." - }, - "usage": { - "input_tokens": 1036, - "output_tokens": 707 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the CLI command 'openclaw agent --channel discord --to ...' delivers to agent:*:main instead of the live Discord channel/thread session because the session resolver does not search by delivery context and Discord targets are not normalized.", - "solution_summary": "Manual patch to compiled dist files added session lookup by delivery context (channel+to+accountId), normalized Discord targets, propagated channel and replyAccount/accountId through the command path, and corrected resolver logic to use buildAgentMainSessionKey; post-patch validation shows CLI now routes to the correct channel/thread sessions.", - "maintainer_signal_summary": "High regression risk: changes were made directly to compiled dist files and will be lost on update; an authoritative source-level fix and tests are required, plus a post-update revalidation script to avoid regressions.", - "dedupe_summary": "CLI 'openclaw agent --channel discord --to' misroutes Discord deliveries to agent:*:main because the session resolver ignores delivery context (channel+to+accountId) and lacks Discord target normalization." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states the CLI misroutes to agent:*:main and identifies the resolver ignoring delivery context and missing Discord target normalization, which is the core issue for deduping." - }, - "usage": { - "input_tokens": 963, - "output_tokens": 697 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions in OpenClaw Discord integration: (1) Discord-thread messages repeatedly had volatile metadata re-injected every turn (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply), causing progressive context bloat and performance degradation; (2) CLI agent commands (openclaw agent --channel discord --to ...) resolved to the default agent:*:main session instead of the live thread-bound session, so commands landed in the wrong session.", - "solution_summary": "Patched compiled runtime bundles: prevented reinjection of volatile Discord thread metadata in pi-embedded; added delivery-target-based session resolution and Discord target normalization in gateway-cli; added delivery-context lookup in the agent command resolver (compact); passed channel and replyAccount/accountId to the resolver and replaced incorrect resolveAgentMainSessionKey usage with buildAgentMainSessionKey(...).", - "maintainer_signal_summary": "Changes are manual edits to compiled files in openclaw/dist and will be lost on update/reinstall, so maintainers should apply the patch or an automated re-application script after updates and restart openclaw-gateway.service; verification shows writer/reasoning sessions producing real toolCall output, threads no longer bloat, and CLI routing now uses the actual Discord target.", - "dedupe_summary": "Discord threads: repeated injection of volatile metadata each turn causing context bloat and CLI agent commands resolving to the main session instead of the thread-bound session due to resolver not using delivery context." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures both core regressions (per-turn re-injection of volatile Discord thread metadata causing context bloat, and CLI commands resolving to the main session because the resolver ignored delivery context) without boilerplate." - }, - "usage": { - "input_tokens": 882, - "output_tokens": 839 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "The sandbox media staging cap is hardcoded as MEDIA_MAX_BYTES = 5 * 1024 * 1024 in dist/store-*.js, causing files larger than 5 MB (PDFs, PPTX, images) to be silently rejected or not copied into sandboxed agents.", - "solution_summary": "Make the staging limit configurable (e.g. support tools.media.maxBytes in openclaw.json) and change the distributed code to read that config instead of using a hardcoded MEDIA_MAX_BYTES constant; preserve or map existing per-type keys as appropriate.", - "maintainer_signal_summary": "This is a regression present since at least v2026.3.7 and still in v2026.3.8; maintainers should remove the hardcoded constant from the distributed build and implement config-driven limits (and provide a patch or release to avoid manual dist edits).", - "dedupe_summary": "Sandbox media staging cap hardcoded to 5MB (MEDIA_MAX_BYTES) preventing files >5MB from being staged; needs a tools.media.maxBytes config option." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focuses on the core problem (hardcoded 5MB MEDIA_MAX_BYTES blocking larger files) and the needed config key (tools.media.maxBytes), with no template noise." - }, - "usage": { - "input_tokens": 929, - "output_tokens": 592 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "OpenClaw's Discord REST API requests fail with \"fetch failed\" despite logs showing the rest proxy is enabled and the Gateway/WebSocket working. The bot receives messages but cannot send replies; direct curl requests through the same proxy succeed, indicating only REST requests from OpenClaw are failing.", - "solution_summary": "Ensure the REST proxy path honors the configured proxy (make fetch or the HTTP agent respect per-provider proxy settings) so Discord REST requests are routed through the proxy; alternatively provide a documented workaround (e.g., using a proxy agent or env vars) until the proxy handling is fixed.", - "maintainer_signal_summary": "Logs show \"rest proxy enabled\" and \"gateway proxy enabled\" but native command deploy and REST calls throw TypeError: fetch failed. Telegram and direct curl via the same proxy work, isolating the issue to OpenClaw's REST fetch/proxy handling rather than the proxy or Discord network.", - "dedupe_summary": "Discord REST API requests in OpenClaw fail with \"fetch failed\" even though rest proxy is enabled and curl via the same proxy works (Gateway/WebSocket unaffected)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (Discord REST requests failing with 'fetch failed' in OpenClaw despite rest proxy being enabled and curl/proxy/Gateway working), with no extraneous template or testing details." - }, - "usage": { - "input_tokens": 874, - "output_tokens": 773 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "Discord REST API calls made via @buape/carbon's RequestClient (and other globalThis.fetch usages like memorySearch) ignore the channels.discord.proxy configured in openclaw.json; only the WebSocket gateway uses the proxy, so REST requests bypass the configured HTTP proxy.", - "solution_summary": "Modify @buape/carbon's RequestClient to accept and use an optional dispatcher/proxy configuration and have OpenClaw pass channels.discord.proxy to it so REST fetches route through the configured proxy; as a temporary workaround, preload an EnvHttpProxyAgent, but do not use NODE_OPTIONS=--use-env-proxy.", - "maintainer_signal_summary": "This is a correctness/security issue causing inconsistent proxy handling and potential network leakage; it requires a change in @buape/carbon to surface dispatcher/proxy configuration and a corresponding OpenClaw change to propagate channels.discord.proxy, with a documented workaround and warning about --use-env-proxy.", - "dedupe_summary": "@buape/carbon RequestClient (Discord REST fetch) ignores openclaw.json channels.discord.proxy and calls globalThis.fetch without a proxy-aware dispatcher." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it identifies the exact component (@buape/carbon RequestClient), the config key ignored (channels.discord.proxy in openclaw.json), and the incorrect behavior (using globalThis.fetch without proxy support), so it will cluster with similar proxy-related REST issues." - }, - "usage": { - "input_tokens": 883, - "output_tokens": 722 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "OpenClaw gateway/session handling and Discord channel lanes experience unbounded conversation context growth (writer sessions ~42k tokens, reasoning ~90k) which increases per-turn latency, long turns block lanes causing serialization under load, and typing indicators can remain stuck until TTL in some code paths, degrading performance under sustained load.", - "solution_summary": "Applied conservative configuration changes: reduced concurrency (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents 8→4, maxChildrenPerAgent 8→4), increased channels.discord.eventQueue.listenerTimeout 120000→300000, set agents.defaults.contextTokens to 80000, improved patching script for hash variants, and restarted the gateway; observed no new lane-wait or typing-TTL errors post-restart but restarts do not fully eliminate issues under sustained load.", - "maintainer_signal_summary": "Restarting the gateway mitigates symptoms but is not a full fix; primary remediation is enforcing context token limits, tightening concurrency controls, and increasing event queue timeouts. Recommend making limits adaptive or exposing them via openclaw config so operators can tune behavior without manual JSON edits.", - "dedupe_summary": "Unbounded conversation context token growth leading to lane blocking/serialization and typing-indicator timeouts in OpenClaw gateway (Discord channel) under sustained high load." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core problem (unbounded context growth causing lane blocking/serialization and typing-indicator timeouts in the OpenClaw Discord gateway) without boilerplate or irrelevant details." - }, - "usage": { - "input_tokens": 817, - "output_tokens": 716 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "OpenClaw v2026.3.11’s Discord thread runtime repeatedly reinjects volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context contamination, increased token usage, broken prompt caching and performance degradation in long threads.", - "solution_summary": "Manual patches to compiled dist: stopped per-turn injection of the listed Discord thread fields in dist/pi-embedded-D6PpOsxP.js, added an isNewSession guard and replaced resolveAgentMainSessionKey with buildAgentMainSessionKey in dist/compact-1mmJ_KWL.js; post-patch validation shows repeated context stopped and prompt caching restored.", - "maintainer_signal_summary": "HIGH risk: fixes were applied directly to compiled dist files and will be lost on update; recommend an official source-level fix, add unit/regression tests, and a post-update revalidation script to detect regressions.", - "dedupe_summary": "Discord threads reinjecting volatile metadata each turn (message_id/reply_to_id/timestamp/Thread starter/history) causing context pollution and prompt cache bust in OpenClaw v2026.3.11" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core bug (Discord thread metadata re-injection causing context pollution and cache busting) but includes a version number which is unnecessary for deduplication." - }, - "usage": { - "input_tokens": 829, - "output_tokens": 745 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord thread messages have volatile metadata re-injected on every turn (message_id, reply_to_id, timestamp, Thread starter, and Chat history since last reply), causing context pollution, prompt cache busting, increased token usage and degraded performance in long threads.", - "solution_summary": "A local hotfix modified the compiled runtime (dist/pi-embedded-D6PpOsxP.js) to inject that Discord thread metadata only on the first turn; validation shows thread context no longer repeats, prompt caching is effective, and token usage is reduced. The fix needs to be applied in source to be permanent.", - "maintainer_signal_summary": "High-priority, high-regression-risk issue because the change was applied to compiled files in dist; please implement the fix in source, ship a patch release, and add a post-update revalidation step to avoid losing the change on upgrades. Affected file: dist/pi-embedded-D6PpOsxP.js.", - "dedupe_summary": "Discord thread metadata re-injected every message by dist/pi-embedded-D6PpOsxP.js (message_id, reply_to_id, timestamp, Thread starter, Chat history) causing context pollution, token bloat and prompt cache busting." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and focused summary naming the exact compiled file, the specific Discord metadata fields being re-injected, and the impact (context pollution, token bloat, prompt cache busting), so it will cluster well with similar reports." - }, - "usage": { - "input_tokens": 837, - "output_tokens": 719 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "OpenClaw runtime for Discord threads is suffering two regressions: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) that causes unbounded context growth, and the session resolver falling back to agent:*:main instead of resolving live thread-bound sessions. User impact is progressive performance degradation and latency growth in long threads, and CLI commands resolving to the wrong session.", - "solution_summary": "Applied runtime patches to compiled bundles (dist/reply-BCcP6j4h.js, dist/compact-1mmJ_KWL.js, dist/pi-embedded-Cz5VjpnY.js, dist/gateway-cli-B-E8XzUM.js, dist/register.agent-jS8B5meo.js) to ensure resolver looks up sessions by delivery context (channel + to + accountId) and to stop repeated metadata reinjection; created idempotent re-application script and backups, restarted and validated the gateway.", - "maintainer_signal_summary": "This is a manual, compiled-bundle hotfix that will be lost on updates; maintainers should merge fixes into source, add the recommended smoke/regression tests (context size per turn and latency), and adopt the reapply script until upstream release includes the changes. Gateway restart and verification checks passed.", - "dedupe_summary": "Discord threads: per-turn metadata duplication causing context growth plus session resolver fallback to agent:*:main, leading to wrong CLI session routing and performance/latency regression." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core bug (per-turn metadata duplication causing context growth) and the related session resolver fallback to agent:*:main that causes misrouted CLI sessions and performance/latency regressions." - }, - "usage": { - "input_tokens": 813, - "output_tokens": 778 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "OpenClaw's Discord integration fails to send reply messages: incoming messages are received and model responses generated, but sending the final reply errors with \"final reply failed: TypeError: fetch failed\" despite the configured Discord HTTP proxy being enabled.", - "solution_summary": "Investigate and fix regression where the Discord REST send path is not using the configured HTTP proxy (or the fetch call changed), causing fetch to fail; ensure the proxy agent/environment is applied to all Discord REST requests and add a regression test.", - "maintainer_signal_summary": "Regression in 2026.3.2 on Node.js 22.22.0; logs show \"discord: rest proxy enabled\", \"discord: gateway proxy enabled\", \"logged in to discord\" followed by \"final reply failed: TypeError: fetch failed\". curl through the same proxy works, indicating proxy reachable but not applied to the final reply path. Reproducible with enabled discord channel proxy in openclaw.json for both DM and group chat.", - "dedupe_summary": "Discord REST send failing with \"final reply failed: TypeError: fetch failed\" even though Discord proxy is configured and enabled (regression in applying proxy to reply/fetch path)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: states the failing Discord REST send with the exact error, notes the proxy is configured/enabled, and identifies it as a regression in applying the proxy to the reply/fetch path, which is ideal for deduplication." - }, - "usage": { - "input_tokens": 753, - "output_tokens": 684 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "The CLI command openclaw agent --channel discord --to resolves the wrong session and sends messages to agent:*:main instead of the live Discord channel/thread session because the compiled session resolver does not look up sessions by delivery context and Discord targets are not normalized.", - "solution_summary": "A hotfix was applied to dist/gateway-cli-BjsM6fWb.js to normalize Discord targets (e.g., 148... → channel:148...), perform session lookup by delivery context (channel + to + accountId) and use that session before falling back to agent:*:main, restoring correct routing to live channel/thread sessions.", - "maintainer_signal_summary": "This is a high-regression-risk manual patch on compiled dist/ files and will be lost on reinstall or upgrade; the fix needs to be implemented in source, covered by tests, and coordinated with related issues for a durable solution.", - "dedupe_summary": "CLI agent --channel discord --to routes to agent:*:main due to session resolver missing delivery-context lookup and lacking Discord target normalization in dist/gateway-cli-BjsM6fWb.js." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that names the CLI command, the incorrect resolution (agent:*:main), the root cause (missing delivery-context lookup and Discord target normalization), and the affected file, so it would cluster with similar routing/session resolver bugs." - }, - "usage": { - "input_tokens": 818, - "output_tokens": 485 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the compiled file dist/compact-1mmJ_KWL.js calls a non-existent function resolveAgentMainSessionKey, causing ReferenceError at runtime; the agent command fails and the gateway can crash when processing agent commands.", - "solution_summary": "A manual patch replaced resolveAgentMainSessionKey(...) with buildAgentMainSessionKey(...) in dist/compact-1mmJ_KWL.js, which removes the ReferenceError and restores correct session resolver and agent behavior; note this edit is to compiled output and will be lost on reinstall.", - "maintainer_signal_summary": "High-priority regression introduced by the prior session-resolver fix (#3); the real fix must be applied to source/build so compiled artifacts use buildAgentMainSessionKey, and the release pipeline should be corrected to prevent reintroducing the error.", - "dedupe_summary": "Regression in compiled dist/compact-1mmJ_KWL.js using undefined resolveAgentMainSessionKey instead of buildAgentMainSessionKey causing runtime ReferenceError and agent command failures." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies the compiled file, the incorrect function name, and the resulting runtime ReferenceError and agent failures with no boilerplate or noise." - }, - "usage": { - "input_tokens": 789, - "output_tokens": 677 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "The Discord channel plugin's proxy setting (channels.discord.proxy) is only applied to WebSocket connections but not to REST API HTTP calls; as a result the plugin health check returns \"fetch failed\" and message send operations time out or fail when a proxy is required, even though curl via the same proxy works.", - "solution_summary": "Update the Discord plugin's REST HTTP client/health-check implementation to honor channels.discord.proxy (e.g., configure the HTTP fetch/agent or proxy settings used for REST calls), ensure both REST and WebSocket traffic go through the configured proxy, and add tests to cover proxy behavior.", - "maintainer_signal_summary": "Regression: previously worked but now fails (OpenClaw 2026.3.8). Repro: configure channels.discord.proxy and run message send or health check; curl via proxy succeeds but OpenClaw REST calls fail with \"fetch failed\". Likely root cause: REST client not using proxy config; needs a prioritized fix.", - "dedupe_summary": "Discord plugin: channels.discord.proxy not applied to REST API calls, causing health check \"fetch failed\" and message-send failures behind a proxy." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific—identifies the exact config key and that proxy setting isn't applied to Discord REST API calls, causing health-check and send failures, so it will cluster correctly with similar reports." - }, - "usage": { - "input_tokens": 745, - "output_tokens": 534 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the session resolver in the compiled file dist/compact-1mmJ_KWL.js for the agent command does not perform lookup by delivery context (channel+to+accountId), causing sessions to resolve to agent:*:main instead of the live session for the target channel. This produces wrong routing compared to the CLI gateway and agent responses are not associated with the correct channel/thread session.", - "solution_summary": "A manual patch was applied to dist/compact-1mmJ_KWL.js to add delivery-context lookup inside the agent command session resolver, searching for an existing session by channel+to+accountId and only falling back to main when no specific session is found, restoring correct routing and response association.", - "maintainer_signal_summary": "This is a hotfix applied to a compiled dist file and has HIGH regression risk because it will be overwritten on update; maintainers should implement the same delivery-context lookup in source and release a new package so the fix persists.", - "dedupe_summary": "Missing delivery-context (channel+to+accountId) lookup in the agent command session resolver in dist/compact-1mmJ_KWL.js causing fallback to agent:*:main and incorrect routing/response association." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the root cause (missing delivery-context lookup in the agent session resolver in dist/compact-1mmJ_KWL.js) and its direct effect on routing, with no extraneous boilerplate." - }, - "usage": { - "input_tokens": 759, - "output_tokens": 800 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English (settings, buttons, agent/skill pages, channel config, status messages), creating significant usability barriers for non-English users, especially Chinese speakers.", - "solution_summary": "Extract all hardcoded UI strings into a structured i18n layer (e.g., JSON locale files), add a zh-CN locale with translated strings, provide a language selector or auto-detect, and build infrastructure to make adding more languages easy; existing docs/zh-CN show translations and the control-ui bundle contains the hardcoded strings to extract.", - "maintainer_signal_summary": "Request to add i18n support (priority medium-high) and to know whether this is on the roadmap or if maintainers welcome contributions/PRs to implement extraction, zh-CN locale, and a language selector.", - "dedupe_summary": "control-ui hardcoded English UI strings — feature request to implement internationalization and add zh-CN localization with language selection." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core issue (control-ui hardcoded English) and requested change (implement i18n, add zh-CN locale, and language selection) without extraneous boilerplate." - }, - "usage": { - "input_tokens": 680, - "output_tokens": 461 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "WhatsApp group conversations could not apply channel-configured systemPrompt instructions at account or group granularity, so users could not tailor reply behavior per WhatsApp account or per group or use wildcard defaults for groups without their own prompt.", - "solution_summary": "Add systemPrompt support to WhatsApp config schema and resolution: support channels.whatsapp.systemPrompt, accounts..systemPrompt, groups..systemPrompt and accounts..groups..systemPrompt; resolve final prompt by composing account prompt (or root) plus resolved group prompt (specific group then wildcard '*'), with account-level groups fully overriding root groups; includes tests and docs.", - "maintainer_signal_summary": "Change is WhatsApp-specific, backward compatible, touches gateway/orchestration and API/config schema, and does not alter non-WhatsApp channels, hot-reload behavior, or group allowlist/mention-gating semantics; tests and docs included and behavior matches Telegram-style override semantics.", - "dedupe_summary": "WhatsApp channel systemPrompt support for per-account and per-group prompts with wildcard fallback and account-level groups replacing root groups" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core change—adding per-account and per-group WhatsApp systemPrompt support with wildcard fallback and account-level group precedence—without extraneous boilerplate." - }, - "usage": { - "input_tokens": 2456, - "output_tokens": 625 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were being dropped before agent processing, and WhatsApp group policy helpers resolved without accountId so account-scoped groupAllowFrom/allowFrom could be ignored leading to gate failures.", - "solution_summary": "Limit append processing to WhatsApp group messages, propagate accountId when resolving WhatsApp group policy, and use account-scoped groupAllowFrom/allowFrom as the fallback; DM append history remains skipped.", - "maintainer_signal_summary": "Changes touch src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts, were validated with live repro logs and targeted tests, and introduce no config/schema/dist/debug changes; scope is explicitly limited to WhatsApp groups.", - "dedupe_summary": "WhatsApp group messages via messages.upsert(type=\"append\") dropped before processing and account-scoped channels.whatsapp.accounts. group settings ignored; fix enables group-append processing and uses accountId for policy resolution." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (WhatsApp group append messages dropped and account-scoped group settings ignored) and the fix (enable group-append processing and use accountId for policy resolution) with no extraneous boilerplate." - }, - "usage": { - "input_tokens": 2008, - "output_tokens": 785 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Gateway auth/network code caused Canvas/A2UI to return 401 when accessed via Tailscale Serve: resolveClientIp returned undefined for trusted-proxy requests that had no forwarding headers, and isLocalDirectRequest did not fall back to req.socket.remoteAddress when allowRealIpFallback was enabled; .ts.net host acceptance was also unconditional.", - "solution_summary": "resolveClientIp now returns the socket address as a fallback when no forwarding headers are present; isLocalDirectRequest checks req.socket.remoteAddress when allowRealIpFallback is enabled; .ts.net host acceptance is gated behind allowTailscale and that flag is threaded through callers; docs updated and tests added.", - "maintainer_signal_summary": "Includes new tests (105 passing) and docs; depends on PR #50636 and closes #28472; allowTailscale still defaults to true when tailscale.mode='serve' to preserve existing setups; reviewers should verify trustedProxies and allowRealIpFallback configs and note the limited risk scope (only when XFF/X-Real-IP are absent).", - "dedupe_summary": "401 for Canvas/A2UI behind Tailscale Serve caused by resolveClientIp/isLocalDirectRequest failing to use socket remoteAddress fallback and unconditional acceptance of .ts.net host headers." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and accurate: it states the core failure (401 behind Tailscale Serve) and the technical root causes (missing socket remoteAddress fallback in resolveClientIp/isLocalDirectRequest and .ts.net host handling), with no boilerplate, so it will cluster correctly." - }, - "usage": { - "input_tokens": 1847, - "output_tokens": 821 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "WhatsApp inbound access control (checkInboundAccessControl) computed an isSelfChat heuristic but did not enforce explicit self-chat mode, allowing non-self messages (group messages, DMs from strangers, broadcasts) through because groupPolicy defaulted to \"open\". This violates the intended self-chat isolation and lets messages reach downstream processing (mention gating, debounce, auto-reply).", - "solution_summary": "Added an early-return guard in checkInboundAccessControl that blocks all non-self messages when account.selfChatMode === true, placed before group/DM policy evaluation. The guard checks only the explicit selfChatMode flag (not the heuristic isSelfChat) to avoid blocking teammates and preserves self-messages; tests pass and the change is limited to WhatsApp access control.", - "maintainer_signal_summary": "Reviewer action: review the new early-return guard in src/web/inbound/access-control.ts and confirm behavior for explicit selfChatMode; no changes to group/DM policy logic, monitor.ts, or other channels. Revert by removing the added guard block if needed; note auto-detected self-chat (heuristic) remains unchanged by design.", - "dedupe_summary": "WhatsApp self-chat mode not enforcing explicit selfChatMode: isSelfChat computed but unused, letting non-self group/DM messages pass due to groupPolicy defaulting to \"open\"." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states the core bug (selfChatMode not enforced because isSelfChat is computed but unused, allowing non-self messages through due to default groupPolicy), which is ideal for deduplication." - }, - "usage": { - "input_tokens": 1562, - "output_tokens": 591 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "With groupPolicy \"allowlist\", group messages from senders not in groupAllowFrom are dropped at the access-control layer and never stored as context, so the agent cannot see them in its \"Chat messages since your last reply\" block despite docs expecting non-triggering messages to be kept for context.", - "solution_summary": "Adds an opt-in flag groupContextFromAll (top-level or per-account, default false) so access-control can return storeForContext for non-allowlisted senders; inbound monitor marks those messages as contextOnly, skips read receipts, and records them as pending group history without triggering replies; config types and schema updated.", - "maintainer_signal_summary": "Default false for zero-breaking change, per-account override recommended, locally tested; changes are localized but there is a potential correctness edge around allowlist normalization and bypassing some group-gating updates that could cause subtle misclassification or state inconsistencies.", - "dedupe_summary": "WhatsApp group allowlist dropping non-allowlisted messages instead of preserving them as pending context; opt-in groupContextFromAll to retain such messages as context-only without generating replies." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (non-allowlisted WhatsApp group messages are dropped instead of stored as context) and the fix (opt-in groupContextFromAll to keep them as context-only), with no boilerplate or noise." - }, - "usage": { - "input_tokens": 1579, - "output_tokens": 744 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "The sessions_spawn sub-agent API is changed to accept an optional sessionKey so a spawn can reuse a deterministic session (short keys map to agent:{agentId}:subagent:{key}, fully-qualified keys are validated against targetAgentId, omission falls back to crypto.randomUUID()). User-facing impact: sub-agents can preserve conversation history across spawns, but Greptile review flagged that session keys may bypass allowlisting and other issues exist in the PR.", - "solution_summary": "Implements an optional sessionKey parameter with name-spacing and validation rules, falls back to UUID when omitted, and adds four tests (deterministic key, UUID fallback, fully-qualified passthrough, cross-agent mismatch rejection) to cover behavior.", - "maintainer_signal_summary": "Blocking issues to fix before merge: a test imports a non-existent module (openclaw-tools.js vs clawdbot-tools.js) so tests won’t compile; cross-agent validation has a case-sensitivity bug (embedded agentId not normalized) causing false rejections; inconsistent error handling (throw vs jsonResult) should be unified.", - "dedupe_summary": "Add sessionKey to sessions_spawn to enable deterministic sub-agent sessions with fully-qualified key validation against agentId; includes a case-sensitivity validation bug and broken test import that block merging." - }, - "judge": { - "score": 3, - "has_boilerplate": true, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Summary correctly highlights the new sessionKey and validation behavior (good for clustering) but injects noisy PR-status/testing details (case-sensitivity validation bug, broken test import) that are irrelevant for deduplication." - }, - "usage": { - "input_tokens": 1632, - "output_tokens": 884 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Telegram adapter: editing messages that contain media (photo, video, audio, document, animation) previously called editMessageText and Telegram returned 400 'there is no text in the message to edit', causing message(action=edit) calls to fail and leave the UI inconsistent.", - "solution_summary": "Updated editMessageTelegram in src/telegram/send.ts to detect the specific 400 error (NO_TEXT_IN_MESSAGE_RE), retry with editMessageCaption for captionable media, and use editMessageReplyMarkup directly for buttons-only updates; MESSAGE_NOT_MODIFIED is treated as success and six unit tests were added.", - "maintainer_signal_summary": "This is a backward-compatible bugfix limited to the Telegram adapter with six new tests; risk is limited to the regex not matching future Telegram error text — failure mode is graceful and the change can be reverted by restoring src/telegram/send.ts; no new permissions or interface changes.", - "dedupe_summary": "Telegram edit failure for captionable media messages due to using editMessageText (400 'no text in message'); add detection and retry to editMessageCaption." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core bug and fix—wrong use of editMessageText for captionable media causing 400 and retrying with editMessageCaption—no boilerplate or noise." - }, - "usage": { - "input_tokens": 1513, - "output_tokens": 747 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "WhatsApp delivery was hardcoded to disable block streaming (PR #24962) and the deliver callback suppressed too many payloads, causing all messages to be batched and sent only when the agent finished even when blockStreaming was enabled in config.", - "solution_summary": "Restore config-driven behavior: make disableBlockStreaming derive from per-account channels.whatsapp.blockStreaming (falling back to agents.defaults.blockStreamingDefault) and change the deliver callback to only suppress kind === \"tool\" payloads; reasoning suppression remains upstream via shouldSuppressReasoningPayload.", - "maintainer_signal_summary": "Fix verified with unit/integration tests and live WhatsApp testing (progressive delivery at text_end); original reasoning/tool-leak fixes remain intact. Quick mitigation: set channels.whatsapp.blockStreaming: false or remove blockStreamingDefault to revert; risk if upstream reasoning filter is removed.", - "dedupe_summary": "whatsapp integration ignoring blockStreaming config (hardcoded disableBlockStreaming: true) causing messages to be batched instead of streamed" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific description of the core bug—WhatsApp integration hardcoded disableBlockStreaming:true causing messages to be batched instead of honoring blockStreaming config—so it will cluster with similar fixes." - }, - "usage": { - "input_tokens": 1505, - "output_tokens": 645 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Sending video or large media via the Telegram channel could cause the gateway to hang and become unresponsive. getFile could succeed but fetchRemoteMedia enforced the old 5MB limit, triggering a content-length error and leaving an unconsumed response body that caused Agent.close()/closeDispatcher to deadlock and drop messages.", - "solution_summary": "Increased default channels.telegram.mediaMaxMb from 5 to 20, fixed closeDispatcher to enforce a 3s timeout with fallback to destroy to avoid undici Agent deadlock, added timeouts and retry logic to getFile/file downloads, made media-download failures non-fatal so captions/text still forward, and extended stripInboundMetadata; tests updated and passing.", - "maintainer_signal_summary": "Backward-compatible change (configurable mediaMaxMb remains), no SSRF policy or new dependencies, relevant code changes in src/infra/net/ssrf.ts and src/telegram/bot.ts, and rollback options include lowering mediaMaxMb or reverting the commit; watch for the 3s cleanup timeout if drains are slow.", - "dedupe_summary": "Telegram gateway hangs due to unconsumed response body / Agent.close deadlock when downloading media exceeding configured mediaMaxMb (default previously 5MB), causing dropped messages and full channel unresponsiveness." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific — identifies the technical root cause (unconsumed response body causing Agent.close deadlock) and the related mediaMaxMb limit that leads to gateway hangs and dropped messages, which will reliably match similar reports." - }, - "usage": { - "input_tokens": 1601, - "output_tokens": 825 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "The Telegram integration's inbound body fallback in resolveTelegramInboundBody used a hardcoded \"\" when no caption/text was present, which mislabels document (PDF/DOCX) and other non-audio media as images, causing agents to behave as if a document wasn't received.", - "solution_summary": "Replace the hardcoded string with the already-computed type-aware placeholder variable so the fallback emits \"\", \"\", etc. as appropriate; unit tests were added to validate placeholder text and document message handling. Media download and file-extraction pipelines were not changed.", - "maintainer_signal_summary": "Unit tests added in extensions/telegram/src/bot/helpers.test.ts and bot.create-telegram-bot.test.ts to lock placeholder behavior and ensure getFile is called for documents; change is small-scope and backward-compatible, with a noted risk that downstream code matching on \"\" may need updating. Closes #7116.", - "dedupe_summary": "Telegram integration fallback uses hardcoded for non-audio media, causing documents to be misidentified instead of using a type-aware placeholder." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, accurate summary of the bug: Telegram fallback used a hardcoded placeholder causing documents to be misidentified instead of a type-aware , with no boilerplate or extraneous details." - }, - "usage": { - "input_tokens": 1428, - "output_tokens": 648 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "The media store (src/media/store.ts) hardcoded MEDIA_MAX_BYTES to 5 MB, so files larger than 5 MB silently failed to stage into sandboxed containers and users/agents could not process common larger files; error messages also referenced the fixed 5MB limit.", - "solution_summary": "Introduced a new optional config field media.maxBytes (capped at 500 MB), replaced the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(), and made the gateway read and apply the configured limit at boot; default remains 5 MB and error messages now reflect the configured limit.", - "maintainer_signal_summary": "Change is backward-compatible (default preserved, deprecated constant still exported), CI/unit tests pass (56 tests), three bot review threads were fixed, and schema enforces the 500 MB cap; touched gateway/orchestration and media store code.", - "dedupe_summary": "Make MEDIA_MAX_BYTES configurable via media.maxBytes to avoid the hardcoded 5 MB staging limit that prevents files >5 MB from being stored in sandboxed agents." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that names the constant and config field, describes the change and its impact (files >5 MB failing to stage), and contains no template noise." - }, - "usage": { - "input_tokens": 1549, - "output_tokens": 648 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "Telegram media download failures (files over mediaMaxMb or exceeding Telegram API limits, or API errors) caused the handler to return early and silently drop the entire message, so the agent received no ack, no message, and any caption/text was lost.", - "solution_summary": "On download failure, the handler now extracts file metadata (name, size, type) from the Telegram message, injects a metadata stub into msg.text describing the failure, and allows the message to continue through the pipeline so the agent sees and can respond to the file event.", - "maintainer_signal_summary": "Change is limited to the Telegram integration (src/telegram/bot-handlers.ts), is backward compatible, removes prior automatic reply-to-user messages, can be reverted by the single commit, and poses low risk since msg.text is only mutated in error paths; no new permissions or network calls.", - "dedupe_summary": "Telegram bot silently drops messages when media file download fails (size limit or API error); inject file metadata stub and continue processing instead of returning early." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (silent drops on Telegram media download failure) and the implemented fix (inject metadata stub and continue processing) without boilerplate." - }, - "usage": { - "input_tokens": 1391, - "output_tokens": 749 - } - }, - { - "thread_id": 5796, - "number": 24117, - "kind": "pull_request", - "title": "feat(cli): add --session-key targeting for openclaw agent", - "summary": { - "problem_summary": "The openclaw agent CLI has no --session-key option, so CLI-triggered deliveries in multi-channel/multi-agent setups can resolve to the agent main session (agent::main) instead of the intended stable channel session because sessionId is UUID-based and --to or --agent are not always usable in callback/automation contexts.", - "solution_summary": "Adds a first-class --session-key flag to openclaw agent, threads sessionKey through AgentCliOpts into resolveSessionKeyForRequest, leaves resolution semantics intact with precedence (sessionKey > sessionId > to > agent), updates help/validation, and adds tests verifying CLI parsing and gateway forwarding.", - "maintainer_signal_summary": "Touches src/cli/program/register.agent.ts and src/commands/agent-via-gateway.ts, updates validation/help text, and adds two tests (register.agent.test.ts and agent-via-gateway.test.ts). Change is additive, backward-compatible, tested, and safe to merge.", - "dedupe_summary": "Add --session-key flag to openclaw agent CLI to target stable session keys and avoid routing to the agent main session when sessionId/--to are unavailable." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the precise change (new --session-key flag for openclaw agent) and its purpose (target stable session keys to avoid fallback routing), with no template noise." - }, - "usage": { - "input_tokens": 1363, - "output_tokens": 513 - } - }, - { - "thread_id": 766, - "number": 11611, - "kind": "pull_request", - "title": "feat: separate group-level allowlist from sender-level command authorization", - "summary": { - "problem_summary": "WhatsApp inbound access-control currently treats groupPolicy: \"allowlist\" with groupAllowFrom as a sender-level gate, blocking or allowing all group interactions based on sender phone number. This prevents allowing whole groups by JID while still restricting slash commands to specific senders, and allows the bot to be pulled into unapproved/spam groups.", - "solution_summary": "If groupPolicy: \"allowlist\" and a channels.whatsapp.groups mapping exists, the access-control now checks group JIDs against that mapping (allowing anyone in approved groups to chat) while keeping groupAllowFrom as the separate sender-level gate for slash commands; when no groups mapping is present the previous sender-based behavior is preserved.", - "maintainer_signal_summary": "Code changes localized to src/web/inbound/access-control.ts and config JSDoc, plus four new Vitest cases covering approved/blocked groups, legacy fallback, and command authorization; backward-compatible fallback preserved and a small doc/comment update required; depends on #3326.", - "dedupe_summary": "WhatsApp group allowlist should gate by group JID from channels.whatsapp.groups (when groupPolicy: \"allowlist\") instead of filtering all group interactions by sender phone number." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core change—use group JIDs from channels.whatsapp.groups to gate access under groupPolicy: 'allowlist' instead of sender phone filtering—so it will cluster with similar changes." - }, - "usage": { - "input_tokens": 1413, - "output_tokens": 605 - } - }, - { - "thread_id": 17924, - "number": 46425, - "kind": "pull_request", - "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", - "summary": { - "problem_summary": "Telegram outbound reply delivery ignored the configured mediaMaxMb: loadWebMedia in the reply path received no maxBytes and fell back to the 100 MB default, so replies could exceed operator-configured limits.", - "solution_summary": "Threaded mediaMaxBytes through the Telegram outbound reply delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path so loadWebMedia receives the configured maxBytes; added unit tests to verify behavior; direct-send path unchanged.", - "maintainer_signal_summary": "Small, backward-compatible bugfix limited to Telegram integration; two unit tests added and all related test suites (108 tests) pass; change is safe to revert by reverting the commit if needed.", - "dedupe_summary": "Telegram outbound reply media ignores mediaMaxMb because loadWebMedia is called without maxBytes in the reply delivery path." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary identifying the exact bug and root cause (mediaMaxMb ignored because loadWebMedia called without maxBytes in the reply delivery path) with no extraneous boilerplate." - }, - "usage": { - "input_tokens": 1414, - "output_tokens": 580 - } - }, - { - "thread_id": 5712, - "number": 37050, - "kind": "pull_request", - "title": "fix(telegram): add HTML parse fallback for media captions", - "summary": { - "problem_summary": "Telegram media send (photo/document/video/animation/audio/voice/video note) captions lacked the HTML-parse fallback that text messages have, so captions containing invalid HTML entities (e.g. unmatched < or >) caused the Telegram API to return a 400 and the entire media send to fail, breaking delivery for users including code snippets or special characters.", - "solution_summary": "The media send callback in src/telegram/send.ts was wrapped with withTelegramHtmlParseFallback so on an HTML parse error the send is retried without parse_mode and with the raw caption; this applies to all media types and a unit test was added to verify the retry behavior.", - "maintainer_signal_summary": "Change is narrowly scoped to the media send inner callback (sendPhoto/sendDocument/sendVideo/sendAnimation/sendAudio/sendVoice/video note); text message, edit message, thread fallback, and caption-splitting logic are unchanged. Tests added mock sendPhoto to assert first call uses HTML and second call retries without parse_mode; change is backward-compatible and reversible by reverting send.ts.", - "dedupe_summary": "Telegram media captions causing 400 \"can't parse entities\" errors due to invalid HTML entities; add retry that sends caption as plain text (no parse_mode)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: identifies Telegram media caption HTML parse errors causing 400s and the added retry fallback to send captions as plain text, without boilerplate or noise." - }, - "usage": { - "input_tokens": 1288, - "output_tokens": 581 - } - }, - { - "thread_id": 21769, - "number": 55280, - "kind": "pull_request", - "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", - "summary": { - "problem_summary": "WhatsApp had block streaming effectively disabled (hardcoded) and a delivery race in the block-reply pipeline: block replies were optimistically enqueued, the pipeline marked them as sent, and the final reply could be suppressed or nothing delivered if async delivery failed. A ternary bug also caused configured blockStreaming to be ignored (undefined vs false).", - "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming, add sensible coalescing defaults, and introduce dispatcher.sendBlockReplyAsync which is awaited so sentContentKeys are only marked after confirmed delivery; fix the ternary so config actually enables streaming; update tests and mocks. ", - "maintainer_signal_summary": "Tests were updated and new tests added for streaming-on/off paths; mocks adjusted for sendBlockReplyAsync and manual verification done. Change fixes the root-cause delivery race (not just the previous workaround) and keeps blockStreaming opt-in by default. ", - "dedupe_summary": "WhatsApp block-streaming disabled/hardcoded and delivery race where optimistic enqueue suppressed final replies; needs config and async-confirmation of block delivery before marking content as sent." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and focused: identifies WhatsApp block-streaming was hardcoded, a delivery race that suppressed final replies, and the fix to make streaming configurable with delivery confirmation, without boilerplate or noisy details." - }, - "usage": { - "input_tokens": 1201, - "output_tokens": 532 - } - }, - { - "thread_id": 8098, - "number": 25543, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked a group-level allowlist option, so groups could only be allowed via sender-level groupAllowFrom, preventing explicit group IDs from bypassing sender checks or running control commands.", - "solution_summary": "Adds channels.signal.groups. config with a SignalGroupConfig type and Zod schema, wires resolveChannelGroupPolicy into the Signal event handler to check group-level allowlist before sender-level checks, and incorporates group-level allow into command authorization; documentation updated.", - "maintainer_signal_summary": "Safe, low-risk change following existing patterns used by other channels; TypeScript compilation passes and manual testing was performed. Supersedes PR #4337 and fixes #25540, with group-level allowlist integrated into both message delivery and command gating.", - "dedupe_summary": "Add channels.signal.groups. group-level allowlist support to bypass sender-level groupAllowFrom and allow control commands for explicitly allowed Signal groups." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core change—adding channels.signal.groups. group-level allowlist that bypasses sender-level groupAllowFrom and enables command authorization for allowed Signal groups—without boilerplate or noise." - }, - "usage": { - "input_tokens": 1143, - "output_tokens": 486 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked group-level allowlisting via channels.signal.groups. With groupPolicy set to \"allowlist\" and groupAllowFrom containing group IDs, the access check compared sender IDs to group IDs and always failed, causing group messages to be silently dropped.", - "solution_summary": "Added a groups config to SignalAccountSchemaBase and updated Signal's group access check to call resolveChannelGroupPolicy before falling back to sender-level groupAllowFrom; added four tests to cover group-level policy resolution.", - "maintainer_signal_summary": "Schema change: added groups to SignalAccountSchemaBase (same shape as other channels). Event handler change: Signal now invokes resolveChannelGroupPolicy for group access checks. Tests: 4 new Signal group-policy tests added and local test suites pass. Size: S.", - "dedupe_summary": "Signal missing channels.signal.groups group-level allowlist support causing groupPolicy: \"allowlist\" with group IDs in groupAllowFrom to drop group messages." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it states Signal lacks channels.signal.groups group-level allowlist support, which causes group messages to be dropped when groupPolicy is set to allowlist with group IDs in groupAllowFrom." - }, - "usage": { - "input_tokens": 943, - "output_tokens": 735 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "WhatsApp channel batches all intermediate agent text into a single final message because process-message.ts hardcodes disableBlockStreaming: true, and queuedFinal was used exclusively to mark delivery so intermediate block payloads are ignored.", - "solution_summary": "Replace the hardcoded flag with a resolveWhatsAppBlockStreamingEnabled helper so channels.whatsapp.blockStreaming controls behavior; when enabled intermediate blocks are delivered as separate WhatsApp messages; register the blockStreaming capability and default in the WhatsApp dock and update tests.", - "maintainer_signal_summary": "Backwards compatible by default (omitted/false keeps current behavior); enabling blockStreaming changes message delivery semantics to emit intermediate blocks, so reviewers should check process-message.ts and dock.ts changes and confirm tests and consumer handling.", - "dedupe_summary": "WhatsApp coalesces intermediate message blocks into a single reply due to hardcoded disableBlockStreaming in process-message.ts." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core bug (hardcoded disableBlockStreaming causing WhatsApp to coalesce intermediate message blocks) without boilerplate, so it will cluster with similar reports." - }, - "usage": { - "input_tokens": 693, - "output_tokens": 648 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v8-embedding-aware.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v8-embedding-aware.json deleted file mode 100644 index 1a9e956..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v8-embedding-aware.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v8-embedding-aware", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "This text is a GitHub issue or pull request. You must produce a summary optimized for text embedding and cosine-similarity-based duplicate detection.\n\nKey insight: the dedupe_summary will be converted to a 3072-dimensional vector. Similar vectors cluster together. Your goal is to maximize the chance that reports about the SAME underlying issue produce SIMILAR vectors, while reports about DIFFERENT issues produce DISSIMILAR vectors.\n\nWhat helps similarity detection:\n- Specific technical terms (API names, component names, error messages)\n- The core behavior being reported or changed\n- Affected subsystem or module names\n\nWhat hurts similarity detection (creates false matches or misses):\n- Generic words: \"bug\", \"fix\", \"issue\", \"problem\", \"update\", \"change\"\n- Template boilerplate that appears in many unrelated issues\n- Testing details, reproduction steps, environment info\n- Version numbers, dates, PR metadata\n\nStrip all boilerplate. Extract only the technical signal.\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nPlain text, no markdown, 1-3 sentences each.", - "timestamp": "2026-03-30T18:21:09.470Z", - "aggregate": { - "avg_score": 4.58, - "avg_signal_density": 4.55, - "boilerplate_count": 2, - "boilerplate_pct": 5, - "would_cluster_correctly_pct": 100, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 47543, - "total_output_tokens": 34681 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "channels.discord.proxy only proxies the Discord WebSocket Gateway connection; startup REST API calls (deploy commands, fetch bot identity) do not use the proxy and time out (“TypeError: fetch failed”), causing guild resolve to fail and guild channels to be non-functional while DMs work.", - "solution_summary": "Apply the configured proxy to the undici dispatcher used for all Discord REST calls at startup (or set a global ProxyAgent/setGlobalDispatcher early), and ensure guild resolve uses the same proxied HTTP client path; consider adding a global gateway.network.proxy config and retry logic for guild resolve.", - "maintainer_signal_summary": "Patch Discord provider startup to set undici's global dispatcher or pass a ProxyAgent for REST requests when channels.discord.proxy is configured; make guild-resolve use the same proxied HTTP client; add config option (gateway.proxy/network.proxy), tests for proxy environments, and retries for guild resolution.", - "dedupe_summary": "channels.discord.proxy only covers WebSocket Gateway, not REST API; Node.js built-in fetch (undici) ignores HTTP_PROXY/HTTPS_PROXY → TypeError: fetch failed / deploy-rest:put timeout; guild resolve uses separate code path and remains unresolved; workaround: setGlobalDispatcher(new ProxyAgent(...)) fixes REST but not guild resolve; suggested: apply ProxyAgent/undici dispatcher for all Discord REST/guild-resolve, add gateway.proxy or network.proxy config and retry logic." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that pinpoints the core bug (channels.discord.proxy applies only to the Gateway, not REST/guild-resolve), identifies the root cause (undici/Node fetch ignoring HTTP_PROXY), notes a workaround, and proposes fixes — ideal for deduplication." - }, - "usage": { - "input_tokens": 2324, - "output_tokens": 737 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "Outbound Discord REST requests bypass configured channels.discord.proxy: WebSocket/gateway connections via proxy work, but REST sends and attachment uploads fail with TypeError: fetch failed. The Carbon RequestClient fetch implementation appears to lack proxy support, so message sends don’t route through the configured HTTP proxy.", - "solution_summary": "Modify RequestClient (Carbon) to pass a proxy agent to fetch (HTTP/SOCKS) so channels.discord.proxy is honored for all REST endpoints including message sends and attachment uploads; add tests and runtime logs to confirm REST requests use the proxy.", - "maintainer_signal_summary": "Investigate the RequestClient/fetch codepath to inject an HTTP/SOCKS proxy agent or use a proxy-aware fetch implementation, ensure channels.discord.proxy config is applied to REST and media transfers, and add logging/tests to verify proxy usage.", - "dedupe_summary": "channels.discord.proxy not applied to outbound REST sends; Carbon RequestClient fetch lacks proxy agent support; WebSocket/gateway uses proxy but REST message send and attachment upload fail with \"TypeError: fetch failed\"; logs show rest proxy enabled but requests bypass proxy; environment requires HTTP proxy (China)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug—Discord REST requests bypass the configured proxy because the RequestClient/fetch lacks proxy agent support—without checklist or repro noise." - }, - "usage": { - "input_tokens": 1006, - "output_tokens": 881 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "When OpenClaw's Discord provider runs behind an HTTP proxy, the gateway/WebSocket logs show successful login but some startup REST calls consistently fail with 'fetch failed', specifically native command deployment and bot identity fetch.", - "solution_summary": "Ensure Discord REST startup calls use the same proxy-aware HTTP transport as the gateway/login path (unify fetch/dispatcher or attach proxy agent to all Discord REST requests) so PUT /applications/{applicationId}/commands and identity fetch go through the configured proxy.", - "maintainer_signal_summary": "Config key: channels.discord.proxy set (HTTP proxy). Logs show 'rest proxy enabled' and 'gateway proxy enabled', fetch-application-id succeeds, but deploy-rest:put fails for path=/applications/{applicationId}/commands with error=fetch failed and 'failed to fetch bot identity: TypeError: fetch failed'; provider still logs 'logged in to discord' and health-monitor later restarts with reason: stale-socket.", - "dedupe_summary": "OpenClaw Discord provider behind HTTP proxy (channels.discord.proxy) — gateway logs 'logged in to discord' and 'rest proxy enabled' but startup REST calls fail: deploy-rest:put path=/applications/{applicationId}/commands error=fetch failed; fetch-bot-identity fails with 'TypeError: fetch failed'; applicationId fetch succeeds; symptom: native command deployment and bot identity fetch fail while WebSocket gateway works — suggests inconsistent proxy handling/unified fetch/dispatcher between gateway/login transport and REST startup requests; leads to health-monitor stale-socket restarts." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that pinpoints the core issue — inconsistent proxy handling for Discord REST calls vs gateway/WebSocket (native command deploy and bot identity fetch failing while login succeeds) — using specific config/key logs that will match duplicates." - }, - "usage": { - "input_tokens": 1152, - "output_tokens": 831 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "OpenClaw's Discord channel logs show proxy enabled but all Discord REST requests fail with \"fetch failed\" and gatewayConnected=false (WebSocket never establishes) when running under WSL2 with an HTTP proxy configured; curl via the same proxy succeeds. The configured proxy is at ~/.openclaw/openclaw.json → channels.discord.proxy and logs show \"discord: rest proxy enabled\" and \"discord: gateway proxy enabled\" but the underlying REST/fetch client is not using the proxy.", - "solution_summary": "Pass a proper HTTP/HTTPS proxy agent into the Discord REST client and gateway initialization (e.g., create an undici/ProxyAgent or HTTP(s)-proxy-agent from channels.discord.proxy and inject it into discord.js/@discordjs/rest and the gateway/WebSocket), or make the code honor HTTP_PROXY/HTTPS_PROXY environment variables. Add unit/integration tests and a WSL2 proxy scenario to verify REST and gateway connectivity through the proxy.", - "maintainer_signal_summary": "Subsystem: gateway/channels/discord. Config key: channels.discord.proxy (~/.openclaw/openclaw.json). Logs: \"discord: rest proxy enabled\", \"discord: gateway proxy enabled\", errors: \"deploy-rest:put:error ... error=fetch failed\" and \"failed to fetch bot identity: TypeError: fetch failed\"; gatewayConnected=false. Likely cause: discord.js/@discordjs/rest (undici/fetch) not receiving a proxy agent. Fix: construct and inject a ProxyAgent or use global/undici agent; ensure both REST and gateway WebSocket use it and add proxy tests (WSL2).", - "dedupe_summary": "OpenClaw Discord channel: proxy config recognized in logs but discord.js REST calls fail with \"fetch failed\" and gatewayConnected=false under WSL2 using HTTP proxy (127.0.0.1:10808). Config path channels.discord.proxy; probable cause: undici/fetch not using proxy agent; solution: inject ProxyAgent/HTTP(S)_PROXY into REST and gateway clients." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core bug (proxy recognized but undici/fetch-based Discord REST calls fail) with relevant technical terms and probable cause/solution, no template noise." - }, - "usage": { - "input_tokens": 989, - "output_tokens": 1150 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "OpenClaw gateway's Discord REST API requests ignore configured proxy (channels.discord.proxy and HTTP(S)_PROXY env vars) and fail with TypeError: fetch failed; Discord WebSocket and other channels (Telegram) work through the proxy.", - "solution_summary": "Route Discord REST API calls through the same proxy-aware SSRF guard/fetch path used by web_search/web_fetch so REST requests honor channels.discord.proxy and HTTP(S)_PROXY/ALL_PROXY settings.", - "maintainer_signal_summary": "Component: gateway/channels/discord; config key: channels.discord.proxy; env vars: HTTP_PROXY, HTTPS_PROXY, ALL_PROXY; error log: \"discord final reply failed: TypeError: fetch failed\"; curl -x proxy to https://discord.com/api/v10/... returns success; Telegram and Discord WebSocket traffic route through proxy correctly; changelog: web tools routed via proxy-aware SSRF guard in 2026.2.26 but Discord REST not included.", - "dedupe_summary": "Discord REST API ignores proxy; TypeError: fetch failed; gateway/channels/discord; channels.discord.proxy; HTTP_PROXY HTTPS_PROXY ALL_PROXY; curl via proxy to discord.com/api v10 succeeds; Telegram OK; Discord WebSocket OK; missing routing through proxy-aware SSRF guard/fetch path used by web_search/web_fetch; request routing fix: send REST calls through shared proxy-aware fetch." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "The summary clearly and concisely states that Discord REST calls ignore the configured proxy and do not use the shared proxy-aware fetch/SSRF path, which precisely captures the core routing bug for clustering." - }, - "usage": { - "input_tokens": 1012, - "output_tokens": 826 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "openclaw CLI command 'openclaw agent --channel discord --to ...' routes messages to agent:*:main instead of the live Discord channel/thread session because the session resolver ignores delivery context and Discord target normalization.", - "solution_summary": "Patched compiled dist files to resolve sessions by real delivery context (channel + to + accountId), normalize Discord targets (prepend 'channel:'), pass channel and replyAccount/accountId through the command path, and use buildAgentMainSessionKey(...) in the agent resolver.", - "maintainer_signal_summary": "Manual fixes were applied to compiled files in dist (gateway-cli, compact, register.agent) and are high-risk because they'll be overwritten on reinstall; an authoritative source-level fix and post-update revalidation are recommended.", - "dedupe_summary": "CLI session resolver bug: 'openclaw agent --channel discord --to' fails to lookup by delivery context (channel+to+accountId) and fails to normalize Discord IDs, causing routing to agent:*:main; patched compiled files dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js to add delivery-context lookup, Discord 'channel:' normalization, pass channel/replyAccount/accountId, and replace resolveAgentMainSessionKey with buildAgentMainSessionKey; fix should be applied in source to avoid regression on update." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "The summary clearly captures the core bug (CLI resolver not looking up by delivery context and failing to normalize Discord IDs, causing routing to agent:*:main) so it will cluster correctly, but it includes unnecessary implementation/compiled-file details and a deployment note that add minor noise." - }, - "usage": { - "input_tokens": 939, - "output_tokens": 542 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions: Discord threads had volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) re-injected every turn causing progressive context bloat; CLI command openclaw agent --channel discord --to delivered but resolved to agent:*:main because the session resolver didn’t look up by actual delivery context (channel + to + accountId).", - "solution_summary": "Patched runtime bundles in openclaw/dist: dist/pi-embedded-... stopped injecting the listed Discord metadata; dist/gateway-cli-... normalizes Discord targets and resolves sessions by delivery target; dist/compact-... adds delivery-context lookup in the agent session resolver; dist/register.agent-... now passes channel and replyAccount/accountId and replaces resolveAgentMainSessionKey with buildAgentMainSessionKey.", - "maintainer_signal_summary": "These are manual edits to compiled bundles in openclaw/dist that will be overwritten on update — keep a re-application script. Restart openclaw-gateway.service to load changes; verification shows new writer/reasoning sessions with real toolCall output and Discord threads no longer re-loading repeated context. Environment: OpenClaw 2026.3.11, Discord, systemd deployment.", - "dedupe_summary": "Discord threads: volatile metadata re-injected every turn (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) causing context bloat; CLI session routing bug where openclaw agent --channel discord --to resolved to agent:*:main because session resolver didn’t use delivery context (channel+to+accountId). Fixes touch runtime bundles (dist/pi-embedded-*, dist/gateway-cli-*, dist/compact-*, dist/register.agent-*) to stop metadata injection, normalize Discord targets, add delivery-context lookup, pass channel/replyAccount/accountId, and replace resolveAgentMainSessionKey with buildAgentMainSessionKey; manual compiled-bundle patch required on upgrades." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that clearly describes the two technical regressions (repeated volatile metadata in Discord threads and CLI session resolver not using delivery context), their causes, and the specific fixes, with no template boilerplate." - }, - "usage": { - "input_tokens": 858, - "output_tokens": 1202 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "dist/store-*.js hardcodes MEDIA_MAX_BYTES = 5 * 1024 * 1024, so files larger than 5MB cannot be staged into sandbox containers (sandbox.mode: \"non-main\") and are silently rejected or unavailable inside the sandbox.", - "solution_summary": "Expose the sandbox staging cap as a configurable setting (e.g. openclaw.json key tools.media.maxBytes) or apply existing per-type limits earlier so the 5MB constant is not the hard global gatekeeper.", - "maintainer_signal_summary": "Regression present since at least v2026.3.7 and still in v2026.3.8; package is distributed as compiled dist files via npm (npm -g openclaw), currently requiring manual sed patches to dist/store-*.js to increase the cap; affects sandboxed agents and causes silent rejections of large media files.", - "dedupe_summary": "dist/store-*.js defines const MEDIA_MAX_BYTES = 5 * 1024 * 1024 causing sandbox staging to reject files >5MB (sandbox.mode non-main); no override in openclaw.json despite existing tools.media.image.maxBytes and tools.media.audio.maxBytes keys; expected new config key tools.media.maxBytes or earlier per-type limit enforcement; regression since v2026.3.7; npm global distribution packs compiled file." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summarizes the core bug (hardcoded 5 MB MEDIA_MAX_BYTES in compiled dist/store-*.js preventing sandbox staging of larger files and lacking an openclaw.json override) but includes minor noise (version and npm packaging mention) that isn't necessary for deduplication." - }, - "usage": { - "input_tokens": 905, - "output_tokens": 877 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "Discord provider's REST API requests fail with \"fetch failed\" despite proxy configured in openclaw.json and logs showing \"rest proxy enabled\"; Gateway WebSocket works and bot receives messages, but REST operations (deploy native commands, fetch bot identity, send messages) throw TypeError: fetch failed while curl with the same proxy succeeds.", - "solution_summary": "Ensure the Discord REST client actually uses the configured proxy: propagate openclaw.json provider/account proxy into the REST/fetch client (use an HTTP proxy agent for undici/Node fetch or set HTTP_PROXY), add diagnostic logging for chosen HTTP agent and outbound connections, and switch to a proxy-aware fetch agent if necessary.", - "maintainer_signal_summary": "Verify openclaw's discord provider passes proxy to its REST client, reproduce with proxy and log agent selection and connection targets, add tests for provider/account proxy precedence, and consider using/adding support for undici/fetch proxy agent or honoring HTTP_PROXY environment variables.", - "dedupe_summary": "discord REST fetch failed; \"rest proxy enabled\" log present; TypeError: fetch failed; failed to deploy native commands / fetch bot identity; websocket gateway works; curl -x proxy to discord API succeeds; openclaw.json discord.proxy and accounts.default.proxy configured; requests likely not routed through Node/undici fetch proxy agent." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core problem (Discord REST requests failing despite proxy configuration and logs, while gateway and curl with proxy work) and the likely cause (requests not using Node/undici proxy agent), so it will match similar issues accurately." - }, - "usage": { - "input_tokens": 850, - "output_tokens": 995 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "Discord REST requests sent via @buape/carbon's RequestClient (and related paths like memorySearch and fetchRemoteMedia) ignore channels.discord.proxy from openclaw.json: the WebSocket gateway uses the proxy but REST calls call globalThis.fetch without a proxy-aware dispatcher, causing requests to bypass the configured HTTP proxy.", - "solution_summary": "Have @buape/carbon RequestClient accept an optional undici dispatcher/proxy option and have OpenClaw pass channels.discord.proxy into the REST client and other fetch callers (memorySearch, attachment downloads). Short-term workaround: preload a module that calls setGlobalDispatcher(new EnvHttpProxyAgent()), but do not use NODE_OPTIONS=--use-env-proxy.", - "maintainer_signal_summary": "Change RequestClient API to accept a dispatcher/proxy and thread channels.discord.proxy from OpenClaw into all REST/fetch code paths; replace direct globalThis.fetch usage or supply a proxy-aware undici dispatcher; ensure attachment downloads and memorySearch use the same dispatcher. Avoid recommending --use-env-proxy since it breaks WebSocket upgrades.", - "dedupe_summary": "@buape/carbon RequestClient calls globalThis.fetch and ignores openclaw.json channels.discord.proxy; Node.js native fetch (Node 22) doesn't honor HTTP_PROXY/HTTPS_PROXY without --use-env-proxy; WebSocket gateway uses proxy but REST and memorySearch/fetchRemoteMedia bypass it; workaround: undici setGlobalDispatcher(new EnvHttpProxyAgent()) via preload; --use-env-proxy breaks ws WebSocket upgrades (disconnect 1005/1006)." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary clearly identifies that @buape/carbon's RequestClient/globalThis.fetch ignores openclaw.json channels.discord.proxy (while websocket uses it) and notes Node fetch/env-proxy behavior and workaround side-effects, which is useful for clustering though it includes some implementation/workaround detail." - }, - "usage": { - "input_tokens": 859, - "output_tokens": 1131 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "Under sustained load with long-running conversations OpenClaw shows unbounded context token growth (writer sessions ~42k tokens, reasoning sessions ~90k), increasing per-turn latency, Discord lane blocking that serializes work under load, and typing indicators remaining stuck until TTL in some code paths.", - "solution_summary": "Applied configuration limits and timeouts: reduced concurrency (acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, agents.defaults.subagents.maxConcurrent 8→4, agents.defaults.subagents.maxChildrenPerAgent 8→4), set agents.defaults.contextTokens=80000, increased channels.discord.eventQueue.listenerTimeout to 300000, improved patching script variants and restarted gateway.", - "maintainer_signal_summary": "Verification: gateway restarted and running, no new 'lane wait exceeded' or 'typing TTL reached' errors observed; patch script updated (openclaw-rootfix-v2026.3.11.sh) to cover reply-*, compact-*, pi-embedded-*, gateway-cli-*, register.agent-* variants. Recommend exposing/adapting context token and concurrency limits as tunable/configurable parameters to prevent recurrence.", - "dedupe_summary": "OpenClaw gateway performance degradation caused by unbounded context token growth (writer ~42k, reasoning ~90k), Discord lane blocking/serialization by agentId, and typing indicators stuck until TTL; mitigations: acp.maxConcurrentSessions 12→6, agents.defaults.maxConcurrent 3→2, subagents maxConcurrent 8→4 and maxChildrenPerAgent 8→4, agents.defaults.contextTokens=80000, channels.discord.eventQueue.listenerTimeout=300000; root causes: no context token limit, high concurrency saturating lanes, insufficient event queue timeout." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary clearly captures the core performance issues (unbounded context growth, Discord lane blocking/serialization, typing stuck) and root causes, but includes specific configuration mitigation values that are slightly noisy for deduping." - }, - "usage": { - "input_tokens": 793, - "output_tokens": 835 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord threads re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every conversation turn, causing context contamination, token bloat, prompt cache invalidation, and session/toolCall regressions.", - "solution_summary": "Manual fix applied to compiled files: dist/pi-embedded-D6PpOsxP.js stops per-turn injection of the listed metadata; dist/compact-1mmJ_KWL.js adds an isNewSession guard to limit Thread starter to the first turn and replaces resolveAgentMainSessionKey with buildAgentMainSessionKey; validation shows writer/reasoning sessions and prompt caching restored.", - "maintainer_signal_summary": "High regression risk — fixes are manual edits to compiled dist/ files and will be lost on update; recommend applying a source-level fix to limit Discord metadata injection to the first turn, add isNewSession guard in the session handler, and add a post-update revalidation script. Affected compiled files: dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js.", - "dedupe_summary": "OpenClaw v2026.3.11 Discord threads per-turn re-injection of volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) in compiled runtime (dist/pi-embedded-D6PpOsxP.js, dist/compact-1mmJ_KWL.js) due to missing isNewSession guard and resolveAgentMainSessionKey misuse, causing context pollution, token inflation and prompt cache bust; manual dist patch prevents injection and uses buildAgentMainSessionKey." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that clearly describes the core bug (per-turn injection of volatile Discord thread metadata), root cause (missing isNewSession/incorrect session key), affected compiled files, and impact (context pollution, token inflation, cache bust), so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 805, - "output_tokens": 1133 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "OpenClaw runtime compiled artifact dist/pi-embedded-D6PpOsxP.js re-injects volatile Discord thread metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, repeated thread-starter/history tokens, prompt-cache busting, token cost increase and degraded performance in long threads.", - "solution_summary": "Manually edited dist/pi-embedded-D6PpOsxP.js to avoid injecting the listed volatile metadata after the first turn (inject only on initial thread message), which restored prompt caching and reduced token usage; recommended to implement the guard in source and upstream the change.", - "maintainer_signal_summary": "High regression risk because the fix modifies a compiled file in dist; maintainers should add the guard in source, add regression tests to prevent per-turn metadata injection for Discord threads, and provide a post-update revalidation script.", - "dedupe_summary": "OpenClaw runtime (dist/pi-embedded-*.js) treats Discord threads as requiring per-turn injection of volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply), causing prompt cache busting, token inflation and degraded performance; temporary manual dist patch prevents re-injection after first turn—needs source-level guard and tests to avoid regressions." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (per-turn injection of volatile Discord thread metadata causing cache busting and token inflation) with minimal noise, though it includes a minor implementation note about a temporary dist patch/tests." - }, - "usage": { - "input_tokens": 813, - "output_tokens": 859 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "Two runtime regressions in Discord threads: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing progressive context growth and latency, and session resolver falling back to agent:*:main so CLI routing resolves the wrong session (openclaw agent --channel discord --to ...).", - "solution_summary": "Applied direct patches to compiled runtime bundles to stop repeated metadata injection and to make resolver lookup use delivery context (channel + to + accountId); deployed idempotent re-application script and backups, restarted gateway and validated thread-bound sessions and logs.", - "maintainer_signal_summary": "Manual edits were applied to compiled dist bundles (reply-*, compact-*, pi-embedded-*, gateway-cli-*, register.agent-*); these patches aren’t preserved on reinstall—maintain the re-application script (/home/clawd/scripts/openclaw-rootfix-v2026.3.11.sh) and backups, and add smoke tests for per-turn context size and latency; gateway runs under systemd and was validated.", - "dedupe_summary": "Discord threads; context growth from per-turn re-injection of message_id, reply_to_id, timestamp, Thread starter, InboundHistory; session resolver fallback to agent:*:main causing CLI routing to wrong session; resolver fix: consistent lookup by delivery context (channel+to+accountId); patched compiled runtime bundles (dist/*: reply, compact, pi-embedded, gateway-cli, register.agent); manual bundle patches lost on update -> idempotent reapply script; systemd openclaw-gateway.service; add smoke tests for latency and context-size." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary that pinpoints two concrete bugs (per-turn metadata re-injection causing context growth and session resolver falling back to global agent), the resolver fix (lookup by delivery context), patched runtime bundles, and operational hardening/tests—no template noise and suitable for deduplication." - }, - "usage": { - "input_tokens": 789, - "output_tokens": 1252 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "OpenClaw 2026.3.2 receives Discord messages and generates responses but fails to send replies with 'TypeError: fetch failed' even though discord proxy is configured and logs show 'rest proxy enabled' and 'gateway proxy enabled'.", - "solution_summary": "Make the Discord REST send path honor the configured HTTP proxy (pass a proxy agent to fetch or respect HTTP_PROXY/HTTPS_PROXY) or update OpenClaw's Discord client to apply the channels.discord.proxy setting for outgoing fetch calls; ensure systemd service exports proxy env. ", - "maintainer_signal_summary": "Regression on Linux systemd (Node.js 22.22.0). openclaw.json has channels.discord.proxy=http://127.0.0.1:7890; logs: 'rest proxy enabled', 'gateway proxy enabled', 'logged in to discord', then 'final reply failed: TypeError: fetch failed'. Incoming messages and model responses are present; curl via same proxy succeeds. ", - "dedupe_summary": "OpenClaw Discord outgoing send failure: 'TypeError: fetch failed' when sending reply despite channels.discord.proxy=http://127.0.0.1:7890 and logs showing rest/gateway proxy enabled; incoming messages and model responses OK; curl via proxy succeeds; Node.js 22.22.0, systemd." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core issue (Discord outgoing sends failing with 'fetch failed' despite proxy settings and logs) and relevant config, but includes minor noisy details (Node.js/systemd version and curl test) that aren't needed for deduping." - }, - "usage": { - "input_tokens": 729, - "output_tokens": 1045 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11, the CLI command 'openclaw agent --channel discord --to ...' resolves to session 'agent:*:main' instead of the live Discord channel/thread session because the compiled session resolver (dist/gateway-cli-BjsM6fWb.js) does not lookup by delivery context (channel+to+accountId) and does not normalize Discord target IDs.", - "solution_summary": "Manually patched dist/gateway-cli-BjsM6fWb.js to normalize Discord targets (e.g. 148... → channel:148...), perform session lookup by delivery context (channel+to+accountId) before falling back to agent:*:main; validated that CLI messages now route to the correct live channel/thread session.", - "maintainer_signal_summary": "Apply the fix in source (not only compiled dist): implement delivery-context-based session lookup and Discord target normalization, add unit/integration tests for '--channel discord --to' routing, and avoid relying on manual edits to dist to prevent regressions.", - "dedupe_summary": "openclaw v2026.3.11 CLI agent --channel discord --to resolves to agent:*:main; root cause: compiled resolver dist/gateway-cli-BjsM6fWb.js lacks delivery-context lookup (channel+to+accountId) and target normalization (148... vs channel:148...), causing fallback to main; manual dist patch adds normalization and lookup; high regression risk because change is in compiled dist." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "The summary concisely states the core bug and technical root cause (missing delivery-context lookup and normalization in the compiled resolver) and would match similar issues, though it includes minor noise like the exact release version and patch/regression notes that aren't necessary for deduplication." - }, - "usage": { - "input_tokens": 794, - "output_tokens": 1090 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "En OpenClaw v2026.3.11 el bundle compilado dist/compact-1mmJ_KWL.js llama a una función inexistente resolveAgentMainSessionKey, provocando ReferenceError en runtime y fallos del comando agent y del session resolver.", - "solution_summary": "Se parcheó dist/compact-1mmJ_KWL.js reemplazando resolveAgentMainSessionKey(...) por buildAgentMainSessionKey(...), lo que elimina el ReferenceError y restaura el comportamiento de agent y las sesiones; la corrección debe aplicarse en el código fuente y reconstruirse para persistir.", - "maintainer_signal_summary": "Corregir la fuente que genera el bundle compact para usar buildAgentMainSessionKey, agregar pruebas que detecten ReferenceError en el session resolver, publicar un build con la corrección y evitar parches directos en dist porque se pierden al reinstalar; esta regresión depende del cambio previo (fix #3).", - "dedupe_summary": "dist/compact-1mmJ_KWL.js; resolveAgentMainSessionKey undefined; buildAgentMainSessionKey; ReferenceError runtime; agent command fails; session resolver regression; compiled dist patch replaced function call; dependent on fix #3; patch lost on reinstall" - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "The summary clearly identifies the core regression (dist/compact-1mmJ_KWL.js calling a non-existent resolveAgentMainSessionKey instead of buildAgentMainSessionKey causing a runtime ReferenceError and agent failures); it’s specific and actionable though it includes a minor extraneous note about dependency on fix #3 and patch persistence on reinstall." - }, - "usage": { - "input_tokens": 765, - "output_tokens": 792 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "Discord channel config channels.discord.proxy is only applied to WebSocket connections, not REST API HTTP calls; as a result openclaw status healthcheck shows 'fetch failed' and openclaw message send times out in proxy-required environments while curl -x through the same proxy succeeds.", - "solution_summary": "Propagate channels.discord.proxy into the Discord REST HTTP client (configure Transport.Proxy or use configured HTTP proxy) and use the same proxy for healthcheck requests; update client initialization and add unit/integration tests to cover proxy scenarios.", - "maintainer_signal_summary": "To reproduce: set channels.discord.proxy, restart gateway, verify curl -x works, then run openclaw message send or openclaw status to observe 'fetch failed'. Inspect the Discord REST client/HTTP transport for missing proxy handling (WebSocket currently uses the proxy) and modify initialization to inject the proxy config.", - "dedupe_summary": "OpenClaw discord plugin: channels.discord.proxy not applied to REST API HTTP client (only applied to WebSocket) causing healthcheck 'fetch failed' and CLI message send failures behind a proxy; curl -x verifies proxy works." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise and specific about the core bug (channels.discord.proxy not applied to REST HTTP client, only WebSocket, causing healthcheck and send failures), but includes a minor testing detail ('curl -x verifies proxy works') that is unnecessary for deduplication." - }, - "usage": { - "input_tokens": 721, - "output_tokens": 889 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "En OpenClaw v2026.3.11 el session resolver dentro de dist/compact-1mmJ_KWL.js del comando `agent` no realiza lookup por delivery context (channel+to+accountId), por lo que resuelve sesiones a `agent:*:main` en lugar de la sesión viva del canal objetivo y las respuestas no se asocian correctamente al hilo/canal.", - "solution_summary": "Parche manual en dist/compact-1mmJ_KWL.js: agregar lookup de sesión por delivery context (channel + to + accountId) y mantener fallback a `agent:*:main` solo si no existe sesión específica.", - "maintainer_signal_summary": "Alta prioridad/regresión: cambio aplicado en archivo compilado en `dist/` (dist/compact-1mmJ_KWL.js) y se perderá en reinstalaciones; requiere correción en código fuente/build para evitar reversiones. Afecta OpenClaw v2026.3.11.", - "dedupe_summary": "session resolver faltando delivery-context lookup (channel+to+accountId) en dist/compact-1mmJ_KWL.js del comando `agent`, causa fallback a agent:*:main, sesiones/respuestas mal asignadas; parche manual en dist compilado; riesgo de pérdida en actualización; versión v2026.3.11." - }, - "judge": { - "score": 4, - "has_boilerplate": true, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary clearly identifies the core bug (missing delivery-context lookup in the agent session resolver in dist/compact-1mmJ_KWL.js causing fallback to agent:*:main and misassigned sessions) but includes minor noisy details (manual patch note and version) that aren't needed for deduplication." - }, - "usage": { - "input_tokens": 735, - "output_tokens": 640 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "OpenClaw Control UI (control-ui) has most interface text hardcoded in English across settings, buttons, agent/skill pages, channel configs, status messages and tooltips, making the UI unusable for Chinese-speaking users.", - "solution_summary": "Extract all hardcoded strings into an i18n layer (JSON locale files), add a zh-CN locale with translated strings, provide a language selector or auto-detect option, and build reusable localization infrastructure for adding more languages.", - "maintainer_signal_summary": "OpenClaw docs already include docs/zh-CN; recommend extracting strings from source (not only dist/control-ui/assets/index-*.js), adopt a standard i18n lib (e.g., i18next or vue-i18n), add zh-CN locale file, and accept contributions to implement the language selector and translations.", - "dedupe_summary": "control-ui hardcoded English strings; extract to i18n JSON locale files; add zh-CN locale; language selector/auto-detect; affected components: Settings, labels, buttons (Add, Remove, Delete, Enable, Disable, Save, Cancel), Agent pages, Skill management, Channel configuration, status messages, tooltips; note dist/control-ui/assets/index-*.js contains hardcoded strings; use i18n/i18next/vue-i18n; goal: Chinese UX accessibility." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core issue (hardcoded English in control-ui), lists affected UI areas and concrete remediation (extract strings, add zh-CN, language selector, use i18n libraries), with no template noise." - }, - "usage": { - "input_tokens": 656, - "output_tokens": 833 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "WhatsApp group chats could not use channel-configured systemPrompt at account or group granularity, preventing per-account and per-group prompt customization and wildcard defaults.", - "solution_summary": "Extended WhatsApp config/schema to support systemPrompt at channels.whatsapp, accounts., and groups. levels; implemented account-level resolution, group prompt composition (account prompt + resolved group prompt joined by blank line), added tests and docs.", - "maintainer_signal_summary": "WhatsApp-only change: adds config keys and resolution logic, tests, and docs; no changes to non-WhatsApp channels, hot-reload, group allowlist/mention-gating, or permissions. Backward compatible; linked to issue #7011.", - "dedupe_summary": "Add support for channels.whatsapp.systemPrompt, channels.whatsapp.accounts..systemPrompt, channels.whatsapp.groups..systemPrompt and channels.whatsapp.accounts..groups..systemPrompt. Resolution: account-level value overrides root; group resolution prefers exact group entry then wildcard \"*\" within the active groups map; account.groups fully replace root.groups (no merge); final injected prompt = account prompt + resolved group prompt (separated by a blank line)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of added WhatsApp systemPrompt config keys and precise resolution/merge rules that capture the core change without boilerplate." - }, - "usage": { - "input_tokens": 2432, - "output_tokens": 674 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages delivered via messages.upsert(type=\"append\") were marked read and dropped before entering the inbound/agent path, and WhatsApp group policy resolution ignored the accountId so account-scoped groupAllowFrom/allowFrom were not considered for allowlist fallback.", - "solution_summary": "Append processing is enabled only for WhatsApp group messages and group policy resolution now propagates accountId so account-scoped channels.whatsapp.accounts. config (groupAllowFrom/allowFrom) is used for group gating; changes touch src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts.", - "maintainer_signal_summary": "DM append history remains skipped (no replay of DM reconnection history); targeted tests added for account-scoped allowlist fallback and require-mention behavior; revert by restoring the two touched files if needed. Watch for duplicate processing of historical DMs or divergent group gating between default and account-scoped accounts.", - "dedupe_summary": "messages.upsert(type=\"append\") WhatsApp group messages dropped before enqueue/inbound, group gating returned shouldProcess:false, group route resolved but accountId not propagated causing channels.whatsapp.accounts. groupAllowFrom/allowFrom to be ignored in groupPolicy:\"allowlist\" fallback; files: src/web/inbound/monitor.ts, src/web/auto-reply/monitor/group-activation.ts; preserve DM append skip, concerns about backlog/history processing, signals: web-inbound, web-auto-reply, lane enqueue." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Clearly states the core bug (WhatsApp group append messages dropped before enqueue and accountId not propagated causing account-scoped groupAllowFrom/allowFrom to be ignored) with specific technical terms, though it includes minor noisy details like file paths and signal tags that aren't necessary for deduping." - }, - "usage": { - "input_tokens": 1984, - "output_tokens": 753 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because resolveClientIp() returned undefined for requests from a trusted proxy that omitted X-Forwarded-* headers, and isLocalDirectRequest() did not fall back to the socket remote address when allowRealIpFallback was enabled and XFF resolved to a non-loopback Tailscale IP.", - "solution_summary": "resolveClientIp() now falls back to req.socket.remoteAddress when no forwarding headers exist; isLocalDirectRequest() checks req.socket.remoteAddress when allowRealIpFallback is enabled; isLocalishHost() gates .ts.net host acceptance behind the existing allowTailscale flag; auth.allowTailscale is threaded through callers; docs updated with trustedProxies and allowRealIpFallback guidance.", - "maintainer_signal_summary": "Updated tests in src/gateway/auth.test.ts and src/gateway/net.test.ts; changed files include src/gateway/auth.ts, net.ts, server-http and related auth/ws handlers; gateway.allowTailscale defaults to true when tailscale.mode=\"serve\"; recommend gateway.trustedProxies [\"127.0.0.1\",\"::1\"] and gateway.allowRealIpFallback: true; watch for behavior changes when proxies omit XFF and for .ts.net gate regressions.", - "dedupe_summary": "Canvas/A2UI 401 via Tailscale Serve; resolveClientIp() returned undefined for trusted-proxy direct requests without X-Forwarded headers; isLocalDirectRequest() lacked req.socket.remoteAddress fallback when gateway.allowRealIpFallback=true; isLocalishHost() now requires auth.allowTailscale to accept .ts.net Host headers; relevant configs: gateway.trustedProxies, gateway.allowRealIpFallback, tailscale.mode; key functions: resolveClientIp, isLocalDirectRequest, isLocalishHost." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that identifies the exact failure (401 via Tailscale Serve), root causes, affected functions, and relevant config flags—no template noise and ideal for clustering similar fixes." - }, - "usage": { - "input_tokens": 1823, - "output_tokens": 859 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "In WhatsApp self-chat mode the function checkInboundAccessControl() computes isSelfChat but never enforces it, so non-self messages (groups, DMs from strangers, broadcasts) can pass because resolveWhatsAppRuntimeGroupPolicy() defaults groupPolicy to \"open\" when cfg.channels.whatsapp is defined without explicit groupPolicy.", - "solution_summary": "Added an early-return guard in checkInboundAccessControl() that returns allowed:false for any non-self message when account.selfChatMode === true; the guard runs before group/DM policy evaluation and preserves self-messages via isSamePhone. The change only applies to explicit selfChatMode:true (not auto-detected heuristic) and keeps isSelfChat in the result for logging/UI.", - "maintainer_signal_summary": "Change touches src/web/inbound/access-control.ts; all access-control tests passed. Revert by removing the early-return guard; intentional design: do not use isSelfChat heuristic to avoid blocking teammates when owner appears in allowFrom.", - "dedupe_summary": "WhatsApp self-chat isolation: checkInboundAccessControl() computed isSelfChat but didn't block non-self messages; resolveWhatsAppRuntimeGroupPolicy() defaulted groupPolicy to \"open\", allowing groups/DMs through. Patch adds an early-return guard when account.selfChatMode === true to return allowed:false for non-self (preserving isSamePhone self messages), applied before policy resolution; explicit-config only, heuristic auto-detected self-chat not hard-blocked. File: src/web/inbound/access-control.ts." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary of the bug (isSelfChat computed but not enforced due to default open groupPolicy) and the targeted fix (early-return guard when account.selfChatMode === true applied before policy resolution), without extraneous checklist or repro details." - }, - "usage": { - "input_tokens": 1538, - "output_tokens": 725 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "With groupPolicy: \"allowlist\", messages from senders not in groupAllowFrom are dropped at the access-control layer before mention-gating runs, so non-allowlisted group messages never get stored in the agent's \"Chat messages since your last reply\" context block.", - "solution_summary": "Introduce opt-in groupContextFromAll (top-level and per-account) so access-control returns storeForContext for non-allowlisted senders; inbound monitor marks those messages contextOnly, skips read receipts, records them via recordPendingHistoryEntryIfEnabled, and stores them as pending context without triggering replies.", - "maintainer_signal_summary": "Changes touch access-control.ts, monitor.ts, types.ts, on-message.ts, config types and zod schema, and accounts; default false preserves backward compatibility; tested locally; risk: allowlist matching/normalization may misclassify senderE164 and the context-only early path may bypass existing group gating/state updates (confidence ~3/5).", - "dedupe_summary": "WhatsApp change: add groupContextFromAll to preserve non-allowlisted group messages as pending context instead of drop. Key tokens: groupPolicy \"allowlist\", groupAllowFrom, groupContextFromAll, access-control -> storeForContext outcome, contextOnly WebInboundMessage, recordPendingHistoryEntryIfEnabled, skip read receipts, no auto-reply trigger; modified files include src/web/inbound/access-control.ts, monitor.ts, types.ts, src/web/auto-reply/monitor/on-message.ts, src/config/types.whatsapp.ts, zod schema, src/web/accounts.ts; risk: allowlist normalization/matching on senderE164." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Clearly describes the core change (preserve non-allowlisted group messages as pending context via new groupContextFromAll and related access-control changes) with specific technical tokens, so it will cluster well despite minor extra file-level detail." - }, - "usage": { - "input_tokens": 1555, - "output_tokens": 892 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "Adds optional sessionKey parameter to sessions_spawn to let sub-agents reuse deterministic sessions instead of generating a new crypto.randomUUID() each spawn; short keys are namespaced to agent:{agentId}:subagent:{key} and fully-qualified keys are validated to match the target agentId. Tests were added but one test imports a non-existent module (openclaw-tools.js), causing compilation failure.", - "solution_summary": "Behavior: if sessionKey omitted fallback to crypto.randomUUID(); short keys map to agent:{agentId}:subagent:{key}; fully-qualified keys (contain ':subagent:') are permitted only if embedded agentId matches normalizeAgentId(targetAgentId) to prevent cross-agent session injection. Fixes: correct the broken test import, normalize embedded agentId case before comparison, and use consistent jsonResult({status:'forbidden',...}) error handling instead of throwing to match existing checks.", - "maintainer_signal_summary": "Blocking test compile: src/agents/openclaw-tools.subagents.sessions-spawn-reuses-session-key.test.ts imports createOpenClawTools from ./openclaw-tools.js (should match other tests' createClawdbotTools ./clawdbot-tools.js). Code file: src/agents/tools/sessions-spawn-tool.ts needs lowercase/normalize embedded agentId for comparison and replace throw new Error(...) with jsonResult({status:'forbidden',...}) for consistent error surface.", - "dedupe_summary": "sessions_spawn: add sessionKey to reuse sub-agent sessions; short keys -> agent:{agentId}:subagent:{key}; fully-qualified keys must contain ':subagent:' and embedded agentId must equal normalizeAgentId(targetAgentId) to prevent cross-agent session injection; current bugs: case-sensitivity mismatch (embedded id not lowercased), inconsistent error handling (throw vs jsonResult), and failing test import openclaw-tools.js vs clawdbot-tools.js." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summarizes the new sessionKey behavior and validation precisely (good for dedupe), but includes implementation bug/test-import details that are useful for PR context yet are minor noise for clustering." - }, - "usage": { - "input_tokens": 1608, - "output_tokens": 1142 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) failed: code called editMessageText and Telegram returned 400 \"there is no text in the message to edit\", causing message(action=edit) to throw and UI inconsistency.", - "solution_summary": "send.ts::editMessageTelegram now catches the specific 400 (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption for captionable media; buttons-only updates use editMessageReplyMarkup directly; HTML-parse fallback preserved and MESSAGE_NOT_MODIFIED is treated as success.", - "maintainer_signal_summary": "Six new unit tests added (auto-detect retry, buttons-only, error propagation, MESSAGE_NOT_MODIFIED); only src/telegram/send.ts changed; backward compatible with no interface changes; risk: regex may break if Telegram error wording changes; revert by restoring previous send.ts implementation.", - "dedupe_summary": "Telegram edit failure: editMessageText -> 400 \"there is no text in the message to edit\"; fix retries with editMessageCaption; buttons-only -> editMessageReplyMarkup; NO_TEXT_IN_MESSAGE_RE regex; MESSAGE_NOT_MODIFIED swallowed; file: src/telegram/send.ts editMessageTelegram; media: photo, video, audio, document, animation; unit tests added." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the bug and fix—detecting captionable Telegram media edits by retrying with editMessageCaption (and handling buttons-only via editMessageReplyMarkup), with relevant symbols/files noted—no boilerplate or noisy details." - }, - "usage": { - "input_tokens": 1489, - "output_tokens": 720 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "PR #24962 hardcoded disableBlockStreaming: true for WhatsApp, which disabled block streaming entirely so messages were batched and sent only when the agent finished even if blockStreamingDefault: \"on\" and blockStreamingBreak: \"text_end\" were configured.", - "solution_summary": "Changed deliver callback to suppress only payloads with kind === \"tool\"; make disableBlockStreaming driven by per-account channels.whatsapp.blockStreaming (falling back to agents.defaults.blockStreamingDefault); reasoning payloads continue to be filtered upstream by shouldSuppressReasoningPayload.", - "maintainer_signal_summary": "Touched files: extensions/whatsapp/src/auto-reply/monitor/process-message.ts and dispatch-from-config.ts (shouldSuppressReasoningPayload at dispatch-from-config.ts:446). Inbound contract tests pass; to revert or disable, set channels.whatsapp.blockStreaming:false or restore process-message.ts. Verify no reasoning/tool payloads leak to WhatsApp.", - "dedupe_summary": "WhatsApp blockStreaming was hardcoded off (disableBlockStreaming:true) causing batched delivery; fix makes disableBlockStreaming config-driven (channels.whatsapp.blockStreaming -> agents.defaults.blockStreamingDefault) and narrows deliver suppression to kind==='tool'; upstream shouldSuppressReasoningPayload in dispatch-from-config.ts still blocks reasoning; files: process-message.ts, dispatch-from-config.ts; behavior: progressive delivery at blockStreamingBreak:text_end vs previous batching." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core bug (hardcoded disableBlockStreaming causing batching), the fix (make it config-driven and limit deliver suppression to kind==='tool'), and relevant files/behavior so it will cluster with similar WhatsApp streaming issues." - }, - "usage": { - "input_tokens": 1481, - "output_tokens": 849 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Telegram gateway hangs when receiving video messages larger than the old 5MB default: fetchRemoteMedia triggers a content-length check ('exceeds maxBytes 5242880') that leaves the response body unconsumed, causing closeDispatcher()/undici Agent.close() to deadlock and the gateway to become unresponsive.", - "solution_summary": "Increase default channels.telegram.mediaMaxMb from 5 to 20; make media download errors non-fatal so text/captions still forward; add timeouts and retry logic to getFile and download flows; change closeDispatcher to use a 3s timeout then destroy the Agent to avoid undici deadlock; update stripInboundMetadata to strip inbound media paths.", - "maintainer_signal_summary": "Touched files: src/infra/net/ssrf.ts, src/telegram/bot.ts; config key: channels.telegram.mediaMaxMb (default now 20); tests updated (strip-inbound-meta.test.ts passes); behavior: inbound media saved to ~/.openclaw/media/inbound/, >20MB yields graceful warning; no SSRF policy or storage changes.", - "dedupe_summary": "Telegram gateway hang on large media: fetchRemoteMedia content-length > maxBytes (5242880) left response body unconsumed -> closeDispatcher()/undici Agent.close() deadlock; fixes: raise channels.telegram.mediaMaxMb 5->20, make media errors non-fatal, add getFile/download timeouts and retries, closeDispatcher 3s timeout + destroy fallback, stripInboundMetadata updated." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Clearly states the root cause (unconsumed response body causing undici Agent.close deadlock) and the specific fixes (raise media limit, non-fatal media errors, timeouts/retries, closeDispatcher fallback, metadata handling), so it will cluster correctly with same issues." - }, - "usage": { - "input_tokens": 1577, - "output_tokens": 851 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "In the Telegram integration, resolveTelegramInboundBody’s fallback branch used a hardcoded '' when no caption/text was present, causing document attachments (PDF/DOCX) to be misidentified as images so the agent failed to recognize documents.", - "solution_summary": "Changed resolveTelegramInboundBody to use the existing type-aware placeholder variable (which yields '', '', etc.) instead of ''; added unit tests to lock in correct placeholders; media download/extraction and understanding pipeline unchanged.", - "maintainer_signal_summary": "Unit tests added in extensions/telegram/src/bot/helpers.test.ts and extensions/telegram/src/bot.create-telegram-bot.test.ts to cover resolveTelegramMediaPlaceholder for all media types and to verify document messages trigger getFile; change is limited to agent-facing body placeholder and is backward compatible.", - "dedupe_summary": "resolveTelegramInboundBody previously hardcoded '' instead of using the computed placeholder variable, mislabeling documents; affected code paths: hasInboundMedia, resolveInboundMediaFileId, getFile invocation. Tests added: extensions/telegram/src/bot/helpers.test.ts and bot.create-telegram-bot.test.ts; media types: document (PDF/DOCX), photo, video, audio, voice, sticker; placeholder strings: '' -> ''." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary clearly identifies the core bug (hardcoded '' vs. computed placeholder causing mislabeling of document/media types) and affected code paths, with minor noise from test filenames and media type lists that don't hurt clustering." - }, - "usage": { - "input_tokens": 1404, - "output_tokens": 860 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "src/media/store.ts contains a hardcoded MEDIA_MAX_BYTES = 5 MB constant so files larger than 5 MB silently fail to stage into sandbox containers, preventing common file types (PDFs, presentations, high-res images) from being available to sandboxed agents. Per-channel download limits and tools.media.* limits are unchanged.", - "solution_summary": "Introduced a new optional config field media.maxBytes (integer bytes, default 5 MB, max 500 MB); replaced hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes() and gateway boot reads the config and sets the store limit; error messages now display the actual configured limit and MEDIA_MAX_BYTES export kept (deprecated) for compatibility.", - "maintainer_signal_summary": "Unit tests updated/passing (store.test.ts, server.test.ts, stage-sandbox-media.test.ts, schema.help.quality.test.ts); addressed review threads by always calling setMediaMaxBytes with a fallback default and adding formatBytesAsMb() for precise error text; schema enforces <=500 MB and default behavior preserved when config absent.", - "dedupe_summary": "Hardcoded MEDIA_MAX_BYTES (5 MB) in src/media/store.ts blocks staging >5MB files into sandbox containers; add configurable media.maxBytes (bytes) read at gateway boot via getMediaMaxBytes()/setMediaMaxBytes(), enforce schema max 500MB, update error text to show configured limit; per-channel download limits unchanged." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that identifies the hardcoded 5MB limit, the exact file and functions changed, the new configurable media.maxBytes behavior, and notes unchanged per-channel limits—no boilerplate or unrelated details." - }, - "usage": { - "input_tokens": 1525, - "output_tokens": 785 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "Telegram media download failures (exceeding mediaMaxMb or Telegram getFile API errors) caused the entire incoming message to be silently dropped, leaving the agent unaware, no ack reaction, and losing any caption/text.", - "solution_summary": "On download failure, extract file metadata (name, size, type) from the Telegram message object, inject a metadata stub into msg.text (e.g., \"⚠️ [File: name, 40.0MB] (exceeds 5MB download limit)\"), and let the message continue to processMessage instead of returning early.", - "maintainer_signal_summary": "Change implemented in src/telegram/bot-handlers.ts; removed early returns on download error paths and added buildFileMetadataStub to mutate msg.text for error-path messages; normal media download path, media group handling, sticker processing, and outbound file sending unchanged; revert by reverting the single commit on bot-handlers.ts.", - "dedupe_summary": "Telegram media download failure -> silent message drop; mediaMaxMb (default 5MB) and Telegram Bot API getFile 20MB limit; inject file metadata stub into msg.text on error (buildFileMetadataStub) so processMessage receives messages including documents/photos/videos/audio/voice/photo_note/sticker with preserved captions; fixes missing ack reaction and lost messages caused by early return in src/telegram/bot-handlers.ts." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the bug and fix — preserves file metadata on Telegram download failure by injecting a stub so messages (documents/photos/voice/stickers, with captions) are not dropped and ack reactions aren't missed; no template noise." - }, - "usage": { - "input_tokens": 1367, - "output_tokens": 693 - } - }, - { - "thread_id": 5796, - "number": 24117, - "kind": "pull_request", - "title": "feat(cli): add --session-key targeting for openclaw agent", - "summary": { - "problem_summary": "openclaw agent CLI has no --session-key option, so CLI-triggered deliveries in callback/automation flows often resolve to the agent main session because sessionId is UUID-based and --to is sender-derived and not always available.", - "solution_summary": "Added --session-key flag in src/cli/program/register.agent.ts, added sessionKey?: string to AgentCliOpts and forwarded it into resolveSessionKeyForRequest via src/commands/agent-via-gateway.ts, preserving resolution precedence (--session-key, --session-id, --to, --agent) and updating help/validation; added tests for parsing and forwarding.", - "maintainer_signal_summary": "Key files: src/cli/program/register.agent.ts and src/commands/agent-via-gateway.ts; tests: src/cli/program/register.agent.test.ts and src/commands/agent-via-gateway.test.ts; change is additive/opt-in, backward-compatible, local tests passing.", - "dedupe_summary": "openclaw agent --session-key sessionKey AgentCliOpts resolveSessionKeyForRequest agent-via-gateway register.agent.ts register.agent.test.ts agent-via-gateway.test.ts precedence --session-key > --session-id > --to > --agent stable channel session targeting callback routing UUID sessionId" - }, - "judge": { - "score": 3, - "has_boilerplate": true, - "signal_density": 3, - "would_cluster_correctly": true, - "reasoning": "Summary identifies the core change (adding --session-key and its precedence to target stable channel sessions vs UUID sessionId) but is cluttered with filenames, test names, and token noise that should be removed for cleaner deduplication." - }, - "usage": { - "input_tokens": 1339, - "output_tokens": 785 - } - }, - { - "thread_id": 766, - "number": 11611, - "kind": "pull_request", - "title": "feat: separate group-level allowlist from sender-level command authorization", - "summary": { - "problem_summary": "With groupPolicy:\"allowlist\" the code currently uses groupAllowFrom (sender phone numbers) to gate all group interactions, preventing per-group JID allowlisting, preventing open chat for everyone in an approved group, and conflating command authorization with group admission (bot can be pulled into spam groups).", - "solution_summary": "If channels.whatsapp.groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access control now checks the group JID allowlist (via resolveChannelGroupPolicy) to admit groups, allows any participant in an approved group to chat, and keeps groupAllowFrom as the sender-level gate solely for slash-command authorization; when no groups mapping exists, behavior falls back to legacy sender-based filtering.", - "maintainer_signal_summary": "Code changes localized to src/web/inbound/access-control.ts (integrates resolveChannelGroupPolicy/checkInboundAccessControl) and JSDoc in src/config/types.whatsapp.ts; adds vitest file src/web/inbound/access-control.group-allowlist.test.ts with four tests covering approved-group admission, blocked groups, legacy sender fallback, and command-authority separation; depends on #3326; one stale inline comment in access-control.ts noted for update.", - "dedupe_summary": "WhatsApp groupPolicy allowlist: when channels.whatsapp.groups mapping is present, gate inbound group messages by group JID allowlist (resolveChannelGroupPolicy) instead of sender-based groupAllowFrom; groupAllowFrom still authorizes slash commands (/new, /reset); fallback to sender-based filtering if no groups mapping; files: src/web/inbound/access-control.ts, src/config/types.whatsapp.ts; tests: access-control.group-allowlist.test.ts." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that clearly describes the behavioral change (use group JID allowlist for group messages, keep sender-based auth for slash commands, fallback behavior) but includes minor noise like filenames/tests that aren't needed for deduping." - }, - "usage": { - "input_tokens": 1389, - "output_tokens": 855 - } - }, - { - "thread_id": 17924, - "number": 46425, - "kind": "pull_request", - "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", - "summary": { - "problem_summary": "Configured telegramAccounts[].mediaMaxMb (mediaMaxBytes) was ignored for outbound reply messages: loadWebMedia() on the reply delivery path received no maxBytes and fell back to the 100 MB default, while the direct-send path already honored the limit.", - "solution_summary": "Propagated mediaMaxBytes through the Telegram outbound reply chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia receives maxBytes; added unit tests verifying propagation and undefined behavior when unset.", - "maintainer_signal_summary": "108 unit tests across delivery, dispatch, native-commands, and bot-message pass; change is backward-compatible, limited to threading a parameter, send.ts and media loading internals unchanged; revertable by a single commit.", - "dedupe_summary": "telegram mediaMaxMb mediaMaxBytes loadWebMedia outbound reply delivery missing maxBytes fallback 100MB threaded through bot.ts bot-message.ts bot-message-dispatch.ts delivery.replies.ts bot-native-commands.ts direct-send send.ts already correct unit tests delivery.test.ts 'passes mediaMaxBytes to loadWebMedia' 'omits maxBytes from loadWebMedia'" - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that states the core bug (mediaMaxMb/mediaMaxBytes not propagated so loadWebMedia falls back to 100MB for outbound Telegram replies) and lists the specific files changed, with no template boilerplate." - }, - "usage": { - "input_tokens": 1390, - "output_tokens": 756 - } - }, - { - "thread_id": 5712, - "number": 37050, - "kind": "pull_request", - "title": "fix(telegram): add HTML parse fallback for media captions", - "summary": { - "problem_summary": "Media send functions (sendPhoto, sendDocument, sendVideo, sendAnimation, sendAudio, sendVoice, video_note) lacked the HTML-parse fallback used for text messages; captions with invalid HTML entities caused Telegram 400 \"can't parse entities\" errors and the entire media send to fail.", - "solution_summary": "Wrapped the media send callback in withTelegramHtmlParseFallback so on HTML parse errors the request is retried without parse_mode (HTML) using the raw caption; change applied in src/telegram/send.ts to cover all media types.", - "maintainer_signal_summary": "Added unit test that mocks sendPhoto to first reject with parse error then succeed, asserting initial call had parse_mode: \"HTML\" and retry had no parse_mode; edit message, text path, thread fallback, and caption-splitting logic unchanged; revert by restoring send.ts sendMedia wrapper.", - "dedupe_summary": "Telegram media captions HTML parse fallback: withTelegramHtmlParseFallback; affected APIs sendPhoto, sendDocument, sendVideo, sendAnimation, sendAudio, sendVoice, video_note; failure: 400 \"can't parse entities\" when parse_mode: HTML; behavior: retry without parse_mode using raw caption; file: src/telegram/send.ts; test: mock sendPhoto reject then succeed asserting parse_mode removed on retry." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Clear, specific summary of adding an HTML-parse fallback for Telegram media captions and the retry behavior on 400 parse errors, but it includes minor noise (file path and test detail) that isn't necessary for deduping." - }, - "usage": { - "input_tokens": 1264, - "output_tokens": 783 - } - }, - { - "thread_id": 21769, - "number": 55280, - "kind": "pull_request", - "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", - "summary": { - "problem_summary": "WhatsApp block streaming was effectively disabled (disableBlockStreaming hardcoded true) so intermediate block replies were batched and a delivery race caused final replies to be suppressed: the pipeline marked sentContentKeys on optimistic enqueue before actual socket delivery, so WhatsApp could receive neither blocks nor final reply.", - "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming and wire disableBlockStreaming from account config; add sendBlockReplyAsync (resolves/rejects on confirmed delivery) and await it in onBlockReply; add blockStreamingCoalesceDefaults and update tests and mocks to ensure sentContentKeys is set only after confirmed delivery.", - "maintainer_signal_summary": "Files changed: process-message.ts (config wiring, disableBlockStreaming fix), shared.ts (blockStreamingCoalesceDefaults), reply-dispatcher.ts (sendBlockReplyAsync, onSettled), dispatch-from-config.ts (await dispatcher.sendBlockReplyAsync), tests updated including dispatch-from-config.test.ts and mock dispatchers; manual verification and config CLI command included.", - "dedupe_summary": "WhatsApp delivery race: optimistic dispatcher.sendBlockReply() enqueued item and pipeline set sentContentKeys before socket confirmation causing suppressed final reply; fix: await dispatcher.sendBlockReplyAsync() that resolves/rejects on confirmed delivery, wire disableBlockStreaming from channels.whatsapp.blockStreaming, add blockStreamingCoalesceDefaults, update process-message.ts, reply-dispatcher.ts, dispatch-from-config.ts and tests." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the WhatsApp delivery race root cause and the concrete fixes (awaiting confirmed block delivery and wiring blockStreaming config), with only relevant file/setting details for clustering." - }, - "usage": { - "input_tokens": 1177, - "output_tokens": 812 - } - }, - { - "thread_id": 8098, - "number": 25543, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked group-level allowlist support: only sender-level groupAllowFrom existed, so specific Signal group IDs could not be explicitly allowed to bypass sender checks or run control commands.", - "solution_summary": "Add channels.signal.groups. config and SignalGroupConfig type; add SignalGroupSchema and groups field to Zod schema; wire resolveChannelGroupPolicy into signal event handler and commandGate authorizers so group-level allowlist bypasses sender-level groupAllowFrom for message delivery and command authorization; update docs.", - "maintainer_signal_summary": "TypeScript compile (tsc --noEmit) passes and manual Signal testing performed. Addresses Greptile review by incorporating explicit group allow into command authorization (commandGate). Supersedes PR #4337 and fixes issue #25540.", - "dedupe_summary": "Add channels.signal.groups. allowlist; SignalGroupConfig (requireMention, tools, toolsBySender); SignalAccountConfig.groups and SignalGroupSchema in Zod schema; resolveChannelGroupPolicy wired into src/signal/monitor/event-handler and used by commandGate authorizers; uses hasGroupAllowFrom param to preserve backward behavior; groups listed bypass sender-level groupAllowFrom checks and are authorized to run control commands." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that clearly states the core change—adding a Signal group-level allowlist/config and wiring it into authorization—without template noise, so it will cluster with similar changes." - }, - "usage": { - "input_tokens": 1119, - "output_tokens": 880 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal lacked group-level allowlisting via channels.signal.groups., so with groupPolicy: \"allowlist\" and groupAllowFrom containing group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter and sender phone/UUID never matches a group ID.", - "solution_summary": "Added groups config to SignalAccountSchemaBase (same shape as other channels) and inserted a resolveChannelGroupPolicy call into Signal's group access check (before the sender-level isSenderAllowed fallback). Added four tests to verify Signal group-policy resolution.", - "maintainer_signal_summary": "Changed SignalAccountSchemaBase to include groups (requireMention, tools, toolsBySender); updated Signal event handler/resolveDmGroupAccessDecision to call resolveChannelGroupPolicy before isSenderAllowed(effectiveGroupAllowFrom); added 4 tests; all related tests pass; fixes #25540.", - "dedupe_summary": "Signal missing resolveChannelGroupPolicy caused groupPolicy:allowlist to use groupAllowFrom (a sender-level filter) against sender phone/UUID, so group IDs in channels.signal.groups. were ignored and group messages dropped; fix adds SignalAccountSchemaBase.groups and calls resolveChannelGroupPolicy in Signal access check." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it identifies the core bug (group allowlist misuse of sender-level filter), the symptom (group messages dropped), and the fix (add groups schema and call resolveChannelGroupPolicy), without boilerplate." - }, - "usage": { - "input_tokens": 919, - "output_tokens": 629 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "process-message.ts hardcodes disableBlockStreaming: true for WhatsApp, causing intermediate agent text/tool blocks to be coalesced into a single final WhatsApp message; intermediate payloads are not counted because queuedFinal was used exclusively to mark delivery.", - "solution_summary": "Add resolveWhatsAppBlockStreamingEnabled() and drive behavior from channels.whatsapp.blockStreaming; replace hardcoded disableBlockStreaming, register blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in dock.ts, and treat intermediate block payloads as valid replies when enabled; tests updated accordingly.", - "maintainer_signal_summary": "process-message.inbound-contract.test.ts updated to mock counts like other channels; all 10 tests pass; change is backward-compatible by default (blockStreaming false); labels: channel: whatsapp-web, app: web-ui, agents, size: S.", - "dedupe_summary": "WhatsApp currently forces disableBlockStreaming:true in process-message.ts so intermediate blocks are coalesced and queuedFinal alone marks delivery; PR adds resolveWhatsAppBlockStreamingEnabled() to use channels.whatsapp.blockStreaming, registers blockStreaming and DEFAULT_BLOCK_STREAMING_COALESCE in dock.ts, and makes intermediate blocks count as delivered when blockStreaming:true (false/omitted preserves legacy coalescing)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change (config-driven WhatsApp block streaming and treating intermediate blocks as delivered) with relevant filenames/functions and no boilerplate." - }, - "usage": { - "input_tokens": 669, - "output_tokens": 838 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/results/v9-issue-vs-pr.json b/.context/compound-engineering/ce-optimize/summary-prompt/results/v9-issue-vs-pr.json deleted file mode 100644 index 640abe2..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/results/v9-issue-vs-pr.json +++ /dev/null @@ -1,940 +0,0 @@ -{ - "experiment_id": "v9-issue-vs-pr", - "model": "gpt-5-mini", - "judge_model": "gpt-5-mini", - "system_prompt": "Summarize this GitHub issue or pull request for duplicate detection clustering.\n\nIf this is an ISSUE:\n- Extract: What specific problem is reported? Which component/API/feature is broken?\n- Ignore: reproduction steps, expected vs actual behavior headings, environment details, template fields\n- dedupe_summary should describe THE PROBLEM in specific technical terms\n\nIf this is a PULL REQUEST:\n- Extract: What specific problem does this fix? What change is being made and to what component?\n- Ignore: testing instructions, review checklists, deployment notes, CI status, diff statistics\n- dedupe_summary should describe THE FIX/CHANGE and what problem it addresses\n\nIn both cases, strip template boilerplate, checklists, and any content that would appear identically in unrelated issues/PRs.\n\nReturn JSON with keys: problem_summary, solution_summary, maintainer_signal_summary, dedupe_summary.\nPlain text, no markdown, 1-3 sentences each. dedupe_summary is most important — it must uniquely identify this topic.", - "timestamp": "2026-03-30T18:38:58.926Z", - "aggregate": { - "avg_score": 4.75, - "avg_signal_density": 4.75, - "boilerplate_count": 1, - "boilerplate_pct": 3, - "would_cluster_correctly_pct": 100, - "total_scored": 40, - "total_errors": 0, - "total_input_tokens": 46676, - "total_output_tokens": 26968 - }, - "results": [ - { - "thread_id": 15126, - "number": 49990, - "kind": "issue", - "title": "Title channels.discord.proxy does not proxy REST API requests — guild resolve fails, guild channels broken behind HTTP proxy", - "summary": { - "problem_summary": "channels.discord.proxy only applies to the WebSocket gateway; startup REST API calls (deploy-rest, fetch-bot-identity) and guild resolution do not use the configured HTTP proxy, causing REST requests to time out (TypeError: fetch failed) and guild channels to remain unresolved in proxy-required environments.", - "solution_summary": "Ensure the configured proxy is applied to the undici/fetch dispatcher used for all Discord REST API calls and the guild-resolve code path during startup (or add a global proxy config), and add retry logic for guild resolution so REST and guild channels work behind an HTTP proxy.", - "maintainer_signal_summary": "Logs show WebSocket connects via proxy but REST calls fail with 'fetch failed' after ~10s and 'channels unresolved: guild:'; Node.js 25.8.0 (undici) fetch ignores HTTP_PROXY; setGlobalDispatcher(new ProxyAgent(...)) makes REST succeed but guild resolve still fails; OpenClaw 2026.3.13.", - "dedupe_summary": "Bug: channels.discord.proxy only proxies Discord Gateway websockets while REST API calls and the guild-resolve path bypass the proxy, causing startup REST timeouts ('fetch failed') and unresolved guild:; workaround using undici ProxyAgent fixes REST but not guild resolve — fix by applying ProxyAgent/global undici dispatcher to all Discord REST and guild-resolve code paths." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that identifies the core bug (proxy only applied to Gateway, REST and guild-resolve bypass it causing timeouts) and the required fix (use undici ProxyAgent/dispatcher for all Discord REST and guild-resolve paths), so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 2302, - "output_tokens": 728 - } - }, - { - "thread_id": 8920, - "number": 30244, - "kind": "issue", - "title": "[Bug]:channels.discord.proxy doesn’t apply to outbound REST sends / Carbon RequestClient fetch has no proxy support", - "summary": { - "problem_summary": "Outbound Discord REST requests (sending messages/attachments) fail when running behind a proxy: channels.discord.proxy is configured but REST sends error with TypeError: fetch failed while the gateway (WebSocket) receives messages successfully.", - "solution_summary": "Add proxy support to the Carbon RequestClient.fetch or otherwise ensure channels.discord.proxy is applied to all outbound Discord REST/HTTP requests (including message sends and attachment uploads) so REST traffic is routed through the configured proxy.", - "maintainer_signal_summary": "Reproducible on OpenClaw 2026.2.25/2026.2.26 in a China/restricted network using http://127.0.0.1:7890/; logs claim 'rest proxy enabled' but REST calls bypass the proxy and fail, causing high-impact failures for users behind proxies/VPNs.", - "dedupe_summary": "channels.discord.proxy is ignored for outbound Discord REST API calls because Carbon's RequestClient.fetch has no proxy support, causing fetch failed errors on message send/attachment uploads in proxy-required environments (OpenClaw 2026.2.25/2026.2.26)." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that identifies the core bug (channels.discord.proxy ignored for outbound REST because Carbon's RequestClient.fetch lacks proxy support) but includes unnecessary version numbers that are irrelevant for deduplication." - }, - "usage": { - "input_tokens": 984, - "output_tokens": 761 - } - }, - { - "thread_id": 19616, - "number": 51018, - "kind": "issue", - "title": "Discord can log in behind proxy, but native command deploy / bot identity fetch fail with 'fetch failed'", - "summary": { - "problem_summary": "When OpenClaw's Discord provider is configured behind an HTTP proxy, the provider can log in and connect the gateway/WebSocket, but some startup REST calls fail with “fetch failed”. Specifically, native command deployment (PUT /applications/.../commands) and bot identity fetch return errors despite application ID fetch and gateway connectivity succeeding.", - "solution_summary": "Ensure all Discord REST startup calls use the same proxy-aware fetch/dispatcher as the gateway/login path; unify or fix transport/proxy handling so native command deployment and bot identity fetch go through the configured HTTP proxy.", - "maintainer_signal_summary": "User provided reproducible logs and environment: OpenClaw 2026.3.12 (touched by 2026.3.13), Ubuntu 24.04, channels.discord.proxy set to http://127.0.0.1:8119; logs show rest proxy enabled, applicationId fetch succeeds, gateway connects, but deploy-rest and fetch-bot-identity fail repeatedly with \"fetch failed\" and later stale-socket restarts; user can help test patches.", - "dedupe_summary": "OpenClaw Discord provider behind HTTP proxy: native command deployment (PUT /applications/:id/commands) and bot identity REST calls fail with \"fetch failed\" while gateway login/WebSocket succeeds, indicating inconsistent or missing proxy handling for some REST request paths (OpenClaw 2026.3.12/3.13, channels.discord.proxy configured)." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary clearly identifies the core problem—Discord REST endpoints (command deployment and identity fetch) failing behind an HTTP proxy while gateway login succeeds—though it includes minor noise like version/config details that aren't needed for deduping." - }, - "usage": { - "input_tokens": 1130, - "output_tokens": 632 - } - }, - { - "thread_id": 16324, - "number": 47670, - "kind": "issue", - "title": "[Bug]: Discord proxy config recognized but REST API fails with fetch failed", - "summary": { - "problem_summary": "Discord channel's configured HTTP proxy (channels.discord.proxy) appears in logs as enabled but the Discord REST client/fetch still fails with 'fetch failed' and the gateway never connects under WSL2. The broken component is the OpenClaw Discord integration (discord.js REST/fetch and gateway connection) on OpenClaw 2026.3.13.", - "solution_summary": "The Discord integration should actually pass and use the configured HTTP proxy (e.g., attach an HTTP proxy agent or configure fetch) for discord.js REST and gateway connections so REST calls succeed and the WebSocket gateway can establish. Currently the proxy is logged as enabled but not applied to the underlying HTTP client.", - "maintainer_signal_summary": "Logs show 'discord: rest proxy enabled' and 'discord: gateway proxy enabled' while REST calls error with 'deploy-rest:put:error ... error=fetch failed' and 'failed to fetch bot identity: TypeError: fetch failed'; gatewayConnected=false and WebSocket never establishes. Curl via the proxy to Discord returns 200, OS is WSL2, OpenClaw 2026.3.13.", - "dedupe_summary": "OpenClaw 2026.3.13 Discord channel proxy not applied: channels.discord.proxy is logged as enabled but discord.js REST/fetch and gateway fail with 'fetch failed' under WSL2 (proxy http://127.0.0.1:10808), preventing REST API calls and WebSocket connection." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary concisely captures the core bug (Discord channel proxy reported enabled but REST/gateway fail with 'fetch failed'), though it includes minor noisy details (specific version and proxy URL) that aren't needed for deduplication." - }, - "usage": { - "input_tokens": 967, - "output_tokens": 944 - } - }, - { - "thread_id": 10106, - "number": 28788, - "kind": "issue", - "title": "Discord REST API ignores proxy configuration", - "summary": { - "problem_summary": "Discord REST API calls from OpenClaw ignore the configured proxy and fail with \"TypeError: fetch failed\" when sending messages, while the Discord WebSocket and other channels (Telegram) work through the proxy and curl against the Discord API via the proxy succeeds.", - "solution_summary": "Modify the Discord channel REST request implementation to use the same proxy-aware SSRF guard/shared fetch wrapper used for web_search/web_fetch so REST calls honor channels.discord.proxy and HTTP_PROXY/HTTPS_PROXY.", - "maintainer_signal_summary": "Reproducible on OpenClaw 2026.2.26 (bc50708) with logs showing \"discord final reply failed: TypeError: fetch failed\"; channels.discord.proxy and environment HTTP_PROXY/HTTPS_PROXY were set and a curl test confirms the proxy itself works; changelog notes web tools were fixed but Discord REST was not included.", - "dedupe_summary": "OpenClaw v2026.2.26: Discord REST API ignores channels.discord.proxy and env HTTP_PROXY/HTTPS_PROXY causing \"TypeError: fetch failed\" for message sends; needs to be routed through the proxy-aware SSRF guard/fetch wrapper introduced for web_search/web_fetch." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Clear, specific summary of the core bug (Discord REST ignoring channel/env proxy causing fetch failures) and suggested fix using the proxy-aware SSRF/fetch wrapper, but it includes a version number which is unnecessary for deduplication." - }, - "usage": { - "input_tokens": 990, - "output_tokens": 803 - } - }, - { - "thread_id": 14855, - "number": 44448, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI 'openclaw agent --channel discord --to' routes to agent:*:main instead of live channel session", - "summary": { - "problem_summary": "openclaw CLI command 'openclaw agent --channel discord --to' delivers to agent:*:main instead of the live Discord channel/thread session because the session resolver does not search by real delivery context (channel+to+accountId) and Discord targets are not normalized (e.g., '148...' vs 'channel:148...'). The issue is present in compiled dist files (dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js).", - "solution_summary": "A manual patch was applied to compiled dist files to add session resolution by delivery target (channel+to+accountId), normalize Discord targets, pass channel and replyAccount/accountId into the resolver, and fix the compact resolver to use buildAgentMainSessionKey and lookup by delivery context so the CLI routes to the correct live session.", - "maintainer_signal_summary": "HIGH regression risk: fixes were made directly in compiled dist files and will be lost on update; maintainers should implement the fix in source, update resolver logic to search by delivery context and normalize Discord targets, and add tests or a post-update revalidation script. Related issues: #36401, #41562, #16149.", - "dedupe_summary": "CLI session resolver bug: 'openclaw agent --channel discord --to' routes to agent:*:main because dist compiled resolvers don't lookup by delivery context (channel+to+accountId) and don't normalize Discord target IDs; patched dist/gateway-cli-*.js, dist/compact-*.js, and dist/register.agent-*.js to add delivery-context lookup and target normalization." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise summary that states the core bug (resolver not using delivery-context and lacking Discord ID normalization), and the compiled files changed, which is the right signal for clustering similar issues." - }, - "usage": { - "input_tokens": 930, - "output_tokens": 941 - } - }, - { - "thread_id": 18179, - "number": 44584, - "kind": "issue", - "title": "[Bug]: Discord threads - volatile metadata re-injection + CLI routing to wrong session", - "summary": { - "problem_summary": "Two regressions in OpenClaw Discord integration: (1) volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) was being re-injected every turn in thread sessions, causing context bloat and degradation; (2) CLI agent commands (openclaw agent --channel discord --to ...) resolved to the generic agent:*:main session instead of the live thread-bound session.", - "solution_summary": "Patched compiled runtime bundles to stop reinjecting volatile Discord thread metadata and to resolve sessions by actual delivery context. Changes include preventing injection of specific metadata in pi-embedded, adding delivery-target normalization and session lookup in gateway-cli and compact modules, and passing channel/replyAccount to the session resolver in register.agent, plus a fix replacing resolveAgentMainSessionKey with buildAgentMainSessionKey.", - "maintainer_signal_summary": "This is a manual patch applied to compiled files in openclaw/dist (dist/pi-embedded-D6PpOsxP.js, dist/gateway-cli-BjsM6fWb.js, dist/compact-1mmJ_KWL.js, dist/register.agent-DHoJWl4M.js). It requires restarting openclaw-gateway.service and will be lost on update/reinstall, so a re-application script or upstream fix is needed.", - "dedupe_summary": "Fix Discord thread regression that re-injected message_id/reply_to_id/timestamp/Thread starter/Chat history every turn and misrouted CLI agent --channel discord --to ... to agent:*:main by adding delivery-context session resolution (channel+to+accountId) and preventing volatile metadata injection; changes applied to dist/pi-embedded-*.js, dist/gateway-cli-*.js, dist/compact-*.js, dist/register.agent-*.js." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the two regressions (volatile metadata re-injection and CLI session misrouting) and the implemented fix (delivery-context session resolution and preventing volatile metadata injection), with no template noise." - }, - "usage": { - "input_tokens": 836, - "output_tokens": 808 - } - }, - { - "thread_id": 2538, - "number": 40880, - "kind": "issue", - "title": "[Bug]: tools/sandbox: MEDIA_MAX_BYTES (5 MB) is hardcoded and not user-configurable", - "summary": { - "problem_summary": "The sandbox media staging cap is hardcoded as MEDIA_MAX_BYTES = 5 * 1024 * 1024 in dist/store-*.js, causing files larger than 5 MB to be silently rejected or unavailable inside sandboxed agents; there is no way to override this via openclaw.json.", - "solution_summary": "Expose the staging cap as a configurable option (e.g. tools.media.maxBytes) or have the runtime read configuration so MEDIA_MAX_BYTES is not hardcoded; alternatively move or honor per-type maxBytes before staging rejection.", - "maintainer_signal_summary": "Regression present since at least v2026.3.7 and still in v2026.3.8; installed via npm on Ubuntu 24; current workaround is a manual sed patch of dist/store-*.js after npm install; causes large-file uploads to fail silently.", - "dedupe_summary": "Hardcoded MEDIA_MAX_BYTES = 5*1024*1024 in dist/store-*.js enforces a 5 MB sandbox staging limit that cannot be configured via openclaw.json (no tools.media.maxBytes), leading to rejection of files >5MB; request to make MEDIA_MAX_BYTES configurable or defer to existing per-type limits." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise description of a hardcoded 5 MB MEDIA_MAX_BYTES in dist/store-*.js that blocks configuration via openclaw.json and requests making the sandbox staging limit configurable, with no template noise and clear technical terms." - }, - "usage": { - "input_tokens": 883, - "output_tokens": 647 - } - }, - { - "thread_id": 9401, - "number": 27409, - "kind": "issue", - "title": "[Bug]: Discord REST API requests fail with \"fetch failed\" despite proxy configuration being enabled", - "summary": { - "problem_summary": "OpenClaw Discord integration cannot send messages: all Discord REST API calls fail with \"fetch failed\" even though REST proxy is configured and logs show \"rest proxy enabled\". The Gateway WebSocket works and bot receives messages, but REST operations (deploying commands, fetching identity, sending replies) error out. Direct curl API calls via the same proxy succeed.", - "solution_summary": "Reporter provides configuration and logs showing rest/gateway proxy enabled and Node/macOS environment; no workaround included beyond demonstrating that curl with the same proxy works. The implied fix is to investigate/fix the REST proxy path or fetch usage in OpenClaw so REST requests honor the configured proxy and succeed.", - "maintainer_signal_summary": "Logs: \"[discord] rest proxy enabled\", \"[discord] gateway proxy enabled\", and repeated \"fetch failed\" / \"TypeError: fetch failed\" during command deployment, identity fetch, and sending replies. OpenClaw version 2026.2.25 on macOS 26.3, Node.js 25.6.1; npm install. Telegram and Gateway WebSocket traffic function correctly while REST fails; curl with -x proxy works.", - "dedupe_summary": "OpenClaw v2026.2.25: Discord REST API requests consistently fail with \"TypeError: fetch failed\" despite rest proxy being configured and logged as enabled; Gateway WebSocket and curl-through-proxy succeed, indicating OpenClaw's REST proxy/fetch integration is broken." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Concise summary that pinpoints OpenClaw's Discord REST requests failing with 'fetch failed' despite proxy enabled and other checks succeeding, but includes an unnecessary version tag which is minor noise." - }, - "usage": { - "input_tokens": 828, - "output_tokens": 540 - } - }, - { - "thread_id": 9156, - "number": 30221, - "kind": "issue", - "title": "Discord REST API (@buape/carbon) ignores channels.discord.proxy config", - "summary": { - "problem_summary": "Discord REST API calls made via @buape/carbon's RequestClient (and some other code paths like memorySearch and attachment downloads) ignore the channels.discord.proxy setting in openclaw.json and call globalThis.fetch without a proxy-aware dispatcher, so they bypass the configured HTTP proxy while the WebSocket gateway correctly uses it.", - "solution_summary": "Have @buape/carbon's REST client accept an optional dispatcher/proxy option (or otherwise use a proxy-aware dispatcher) and update OpenClaw to pass channels.discord.proxy into that client; as a temporary workaround, preload undici's EnvHttpProxyAgent to set the global dispatcher instead of using NODE_OPTIONS=--use-env-proxy which breaks WebSocket connections.", - "maintainer_signal_summary": "Root cause is Node.js 22 native fetch not honoring HTTP(S)_PROXY env vars unless --use-env-proxy is enabled, and carbon's RequestClient calls globalThis.fetch with no dispatcher; enabling --use-env-proxy breaks Discord WebSocket upgrades, so a code change in carbon/OpenClaw is required to propagate proxy config safely. Related: issue #25232 for attachment downloads bypassing proxy.", - "dedupe_summary": "@buape/carbon RequestClient and other REST/fetch paths ignore openclaw.json channels.discord.proxy (they call globalThis.fetch without a proxy dispatcher), causing Discord REST calls and media/embedding fetches to bypass the configured HTTP proxy on Node 22; WebSocket uses the proxy properly, and using --use-env-proxy is not viable because it breaks WebSocket connections." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Clear, focused summary of the core bug (RequestClient and other fetch paths ignore channels.discord.proxy and call globalThis.fetch, bypassing the HTTP proxy) with minor extra detail (Node 22 / --use-env-proxy) that isn't necessary but doesn't harm clustering." - }, - "usage": { - "input_tokens": 837, - "output_tokens": 606 - } - }, - { - "thread_id": 18848, - "number": 44582, - "kind": "issue", - "title": "[Bug]: Performance degradation - context growth + lane blocking under load", - "summary": { - "problem_summary": "OpenClaw gateway degrades under sustained load from long-running conversations: per-session context token growth (writer ~42k tokens, reasoning ~90k) causes increasing per-turn latency, Discord lanes get blocked causing serialization under load, and typing indicators can remain stuck until TTL.", - "solution_summary": "Applied configuration limits: reduced maxConcurrentSessions from 12→6, agents.defaults.maxConcurrent 3→2, subagents.maxConcurrent 8→4, subagents.maxChildrenPerAgent 8→4; set agents.defaults.contextTokens to 80000; increased channels.discord.eventQueue.listenerTimeout from 120000→300000; improved patching script to cover hash variants and restarted the gateway.", - "maintainer_signal_summary": "Gateway restart confirmed active and no new \"lane wait exceeded\" or \"typing TTL reached\" errors; however restart is a mitigation and sustained-load risk remains. Recommendation: make context/concurrency limits adaptive or expose them in openclaw config for easier tuning.", - "dedupe_summary": "Unbounded per-session context token growth (writer ~42k, reasoning ~90k) plus high concurrency saturates lanes and causes lane blocking/serialization and stuck typing indicators; root causes are missing context token limits, excessive concurrency settings, and insufficient event-queue listener timeout." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core issues (unbounded per-session context growth, concurrency saturating lanes causing blocking/serialization, and stuck typing indicators) and their root causes without template noise, so it will cluster with similar issues." - }, - "usage": { - "input_tokens": 771, - "output_tokens": 747 - } - }, - { - "thread_id": 14856, - "number": 44447, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread context re-injected every turn (context pollution + cache bust)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11, Discord thread conversations repeatedly re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) on every turn, causing context contamination, token bloat and breaking prompt cache behavior. The compiled runtime metadata injector and session handler in dist/ are responsible (missing an isNewSession guard).", - "solution_summary": "A manual patch was applied to compiled files: dist/pi-embedded-D6PpOsxP.js was changed to stop re-injecting message_id, reply_to_id, timestamp, Thread starter and Chat history on each turn; dist/compact-1mmJ_KWL.js was updated to add an isNewSession guard for thread-starter injection and to replace resolveAgentMainSessionKey with buildAgentMainSessionKey. Post-patch validation shows thread context no longer repeats and prompt caching works.", - "maintainer_signal_summary": "HIGH regression risk: changes were made directly in compiled dist/ artifacts and will be lost on updates; maintainers should implement the fix in source, add a regression test and a post-update validation script, and review related issues (#41355, #34228, #37047).", - "dedupe_summary": "Discord thread metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history) is being re-injected every turn by the compiled runtime because the session handler lacks an isNewSession guard (files dist/pi-embedded-D6PpOsxP.js and dist/compact-1mmJ_KWL.js), causing context pollution, token growth and prompt cache busting." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that identifies the exact metadata re-injected, the missing isNewSession guard, affected compiled files, and the concrete impacts, so it will cluster with similar runtime/metadata injection bugs." - }, - "usage": { - "input_tokens": 783, - "output_tokens": 667 - } - }, - { - "thread_id": 14863, - "number": 44449, - "kind": "issue", - "title": "[Bug] v2026.3.11: Discord thread metadata re-injected every turn (context pollution)", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 Discord thread messages re-inject volatile metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every turn, causing context pollution, prompt cache busting, token bloat and performance degradation in long threads.", - "solution_summary": "A manual hotfix edited the compiled file dist/pi-embedded-D6PpOsxP.js to stop unconditional per-turn injection and only inject the volatile Discord thread metadata on the first turn; validation shows repeated context stops, prompt caching is restored and token usage falls. Recommended: apply an official fix in source and add a revalidation script to reapply after updates.", - "maintainer_signal_summary": "High regression risk because the change was made in a compiled dist file and will be lost on reinstall/upgrade; related issues (#41355, #34228, #37047) indicate recurrence and the problem has cost/performance impact, so an upstream patch and deployment plan are urgent.", - "dedupe_summary": "OpenClaw v2026.3.11 bug: dist/pi-embedded-D6PpOsxP.js unconditionally re-injects Discord thread metadata (message_id, reply_to_id, timestamp, Thread starter, Chat history since last reply) on every message turn; fix is to inject those fields only on the thread's first turn to avoid context pollution and cache busting." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and focused: states the exact bug (unconditional re-injection of Discord thread metadata), lists the affected file and fields, and notes the intended fix to inject only on the first turn, with no template noise." - }, - "usage": { - "input_tokens": 791, - "output_tokens": 737 - } - }, - { - "thread_id": 18847, - "number": 44586, - "kind": "issue", - "title": "[Bug]: Discord threads root fix - runtime patches for context growth + session routing", - "summary": { - "problem_summary": "Two runtime regressions in OpenClaw’s Discord threads: per-turn re-injection of metadata (message_id, reply_to_id, timestamp, Thread starter, InboundHistory) causing unbounded context growth, and the session resolver falling back to agent:*:main instead of selecting a live thread-bound session (routing by channel+to+accountId). These originate from missing patches in compiled runtime bundles (dist/*.js).", - "solution_summary": "Manual patches applied to compiled runtime bundles (dist/reply-BCcP6j4h.js, dist/compact-1mmJ_KWL.js, dist/pi-embedded-Cz5VjpnY.js, dist/gateway-cli-B-E8XzUM.js, dist/register.agent-jS8B5meo.js) to stop metadata re-injection and make the CLI/resolver consistently lookup sessions by delivery context; gateway restarted and validated. Operational hardening added: automatic .bak backups and an idempotent re-application script to restore fixes after updates, plus recommended regression tests.", - "maintainer_signal_summary": "Gateway restarted and is active; backups created (*.bak-rootfix-20260313T023340Z); idempotent reapply script at /home/clawd/scripts/openclaw-rootfix-v2026.3.11.sh; verification checklist passed (no new errors, thread-bound sessions resolve correctly, CLI routing fixed). Note: changes are manual edits to compiled bundles and will be lost on reinstall—reapplication script required on updates.", - "dedupe_summary": "OpenClaw 2026.3.11 Discord-thread runtime bug: compiled dist JS lacked runtime patches so each turn re-injected message/thread metadata causing context bloat, and session resolver didn’t lookup by delivery context (channel+to+accountId) causing fallback to agent:*:main; manually patched dist/*.js, created backups and a reapply script to mitigate but fix must be preserved in source/build to be permanent." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "The summary concisely captures the core technical issues (missing runtime patches causing per-turn metadata re-injection/context bloat and a session resolver lookup bug) and remediation, but includes minor extraneous details (a version and manual mitigation steps) that aren’t needed for deduplication." - }, - "usage": { - "input_tokens": 767, - "output_tokens": 670 - } - }, - { - "thread_id": 5022, - "number": 37344, - "kind": "issue", - "title": "[Bug]: Discord messages not being sent - \"fetch failed\" despite proxy configuration", - "summary": { - "problem_summary": "OpenClaw 2026.3.2 fails to send Discord reply messages: the bot receives messages and the model generates responses, but sending the reply errors with \"final reply failed: TypeError: fetch failed\" despite Discord proxy being configured.", - "solution_summary": "User reports the Discord proxy is set in openclaw.json and logs show rest/gateway proxy enabled, and curl through the same proxy succeeds; they suspect OpenClaw's Discord REST request for final replies is not applying the configured proxy.", - "maintainer_signal_summary": "Regression (worked before); Node 22.22.0 on Linux via systemd; logs show \"rest proxy enabled\", \"gateway proxy enabled\", \"logged in to discord\" then \"final reply failed: TypeError: fetch failed\"; reproduces in DM and group chats; proxy verified separately with curl.", - "dedupe_summary": "OpenClaw 2026.3.2 Discord outgoing reply failure: \"final reply failed: TypeError: fetch failed\" even though openclaw.json has discord.proxy (http://127.0.0.1:7890) and logs show rest/gateway proxy enabled; proxy works for curl, indicating final reply fetch likely not using configured proxy." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Summary clearly captures the core bug (Discord outgoing replies failing with a fetch error likely due to proxy not being used) but includes minor noise like the specific OpenClaw version and proxy URL that aren't needed for deduplication." - }, - "usage": { - "input_tokens": 707, - "output_tokens": 838 - } - }, - { - "thread_id": 14862, - "number": 44450, - "kind": "issue", - "title": "[Bug] v2026.3.11: CLI agent --channel discord --to routes to agent:*:main instead of live session", - "summary": { - "problem_summary": "En OpenClaw v2026.3.11 el CLI `openclaw agent --channel discord --to ...` resuelve incorrectamente a la sesión global `agent:*:main` en lugar de la sesión viva del canal/thread Discord objetivo; el resolver de sesiones en el archivo compilado dist/gateway-cli-BjsM6fWb.js no busca por delivery context ni normaliza targets Discord.", - "solution_summary": "Se parcheó manualmente dist/gateway-cli-BjsM6fWb.js para: normalizar targets Discord (p.ej. `148...` → `channel:148...`), resolver sesiones buscando por delivery context compuesto (channel + to + accountId) antes de hacer fallback a `agent:*:main`; validación local confirma enrutamiento correcto.", - "maintainer_signal_summary": "Alto riesgo de regresión: el cambio es sobre archivos compilados en dist/ y se perderá en actualizaciones; requiere implementar la corrección en el código fuente/build pipeline y no solo en dist. Referencias relacionadas: #36401, #41562.", - "dedupe_summary": "CLI `agent --channel discord --to` enruta a agent:*:main porque dist/gateway-cli-BjsM6fWb.js no normaliza targets Discord ni hace lookup por delivery context (channel+to+accountId); manual patch adds normalization and delivery-context session lookup to prevent fallback to agent:*:main." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that identifies the core bug (CLI routes to agent:*:main due to missing Discord target normalization and lack of delivery-context lookup in dist/gateway-cli-BjsM6fWb.js) and notes the applied fix, so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 772, - "output_tokens": 678 - } - }, - { - "thread_id": 14859, - "number": 44453, - "kind": "issue", - "title": "[Bug] v2026.3.11: Regression in compact - resolveAgentMainSessionKey ReferenceError", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 a regression in the compiled bundle dist/compact-1mmJ_KWL.js calls a non-existent function resolveAgentMainSessionKey, causing ReferenceError at runtime; the agent command fails and gateways may crash when processing agent commands.", - "solution_summary": "Manually edited dist/compact-1mmJ_KWL.js to replace resolveAgentMainSessionKey(...) with the correct buildAgentMainSessionKey(...); this removes the ReferenceError and restores agent/session resolver behavior, but the change is in compiled output and will be overwritten by updates.", - "maintainer_signal_summary": "High-priority regression: fix is currently only in compiled dist file and will be lost on reinstall/upgrades; the source-level code and build output must be corrected and the session-resolver fix (#3) must be applied together with this change to avoid ReferenceError.", - "dedupe_summary": "Regression in v2026.3.11 dist/compact-1mmJ_KWL.js: incorrect use of resolveAgentMainSessionKey instead of buildAgentMainSessionKey caused ReferenceError and agent command failures; temporary manual fix replaces the call in the compiled file but requires a repo/build-level correction to be permanent." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the regression: wrong function used in compiled file causing ReferenceError and temporary local fix, which correctly highlights the code-level change needed for deduplication." - }, - "usage": { - "input_tokens": 743, - "output_tokens": 539 - } - }, - { - "thread_id": 14142, - "number": 42702, - "kind": "issue", - "title": "[Bug]: Discord plugin fails with \"fetch failed\" when using proxy", - "summary": { - "problem_summary": "Discord channel plugin's REST API HTTP requests are not using the channels.discord.proxy configuration, causing health checks to return 'fetch failed' and message send operations to time out even though the WebSocket connection uses the proxy and curl via the same proxy works.", - "solution_summary": "Ensure the channels.discord.proxy setting is applied to the Discord REST API HTTP client (in addition to WebSocket connections) so health checks and openclaw message send calls route through the configured proxy and succeed.", - "maintainer_signal_summary": "Regression reported on OpenClaw 2026.3.8 (commit 3caab92) running on Ubuntu 24.04; reproduces reliably where a proxy is required and manifests as failed health checks and message delivery despite external curl via proxy succeeding; labeled bug and regression.", - "dedupe_summary": "discord plugin REST API ignores channels.discord.proxy (only websocket uses it), causing 'fetch failed' health checks and message send failures in proxy-required environments (OpenClaw 2026.3.8)." - }, - "judge": { - "score": 4, - "has_boilerplate": false, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Clear, specific summary of the core bug (REST API ignoring channels.discord.proxy causing fetch failures), but includes an unnecessary version number which is minor noise." - }, - "usage": { - "input_tokens": 699, - "output_tokens": 627 - } - }, - { - "thread_id": 14861, - "number": 44451, - "kind": "issue", - "title": "[Bug] v2026.3.11: Session resolver in compact lacks delivery-context lookup", - "summary": { - "problem_summary": "In OpenClaw v2026.3.11 the session resolver inside the agent command in the compiled file dist/compact-1mmJ_KWL.js does not perform a delivery-context lookup (channel+to+accountId), so sessions resolve to agent:*:main instead of the live target channel session and responses are misassociated.", - "solution_summary": "A manual patch was applied to dist/compact-1mmJ_KWL.js adding a lookup for an existing session by delivery context (channel + to + accountId) and only falling back to main if no specific session is found; validation shows routing and response association now correct.", - "maintainer_signal_summary": "High regression risk because the fix edits a compiled file in dist/ and will be overwritten by updates/reinstalls; affected version v2026.3.11—this requires a source-level fix and release to be permanent.", - "dedupe_summary": "dist/compact-1mmJ_KWL.js session resolver missing delivery-context (channel+to+accountId) lookup causes agent command to default to agent:*:main; manual compiled-file patch added delivery-context lookup but needs canonical fix in source and release." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that names the compiled file, the exact missing delivery-context lookup (channel+to+accountId), the wrong fallback behavior (agent:*:main), and notes a manual patch while calling for a source/release fix—suitable for deduplication." - }, - "usage": { - "input_tokens": 713, - "output_tokens": 606 - } - }, - { - "thread_id": 21902, - "number": 55855, - "kind": "issue", - "title": "[Feature Request] Add i18n support for OpenClaw Control UI (Chinese localization)", - "summary": { - "problem_summary": "The OpenClaw Control UI has most interface text hardcoded in English, causing usability barriers for non-English users (settings, buttons, agent/skill pages, channel config, status messages, tooltips).", - "solution_summary": "Extract hardcoded strings into an i18n layer (e.g., JSON locale files), add a zh-CN locale with translations, provide a language selector or auto-detect system language, and build infrastructure to support additional locales going forward.", - "maintainer_signal_summary": "Requester notes docs already have zh-CN translations, points out hardcoded strings in the bundled control-ui assets, marks priority Medium-High for UX/accessibility, and offers to hear about roadmap or contribute to implementation.", - "dedupe_summary": "Feature request to add Simplified Chinese (zh-CN) i18n to the OpenClaw Control UI by extracting hardcoded English strings into locale files, adding zh-CN translations, and adding a language selector/auto-detection so the UI supports multiple languages." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core feature request—adding zh-CN i18n to OpenClaw Control UI by extracting hardcoded strings into locale files and adding language selection/auto-detection—without extraneous template text." - }, - "usage": { - "input_tokens": 634, - "output_tokens": 441 - } - }, - { - "thread_id": 22366, - "number": 55328, - "kind": "pull_request", - "title": "feat(whatsapp): add systemPrompt support for accounts and groups ", - "summary": { - "problem_summary": "WhatsApp group chats could not apply channel-configured systemPrompt instructions at account or group granularity, preventing per-account and per-group prompt customization (including wildcard defaults). The issue affected the gateway/integration configuration/schema and runtime prompt injection for WhatsApp groups.", - "solution_summary": "Adds systemPrompt support to channels.whatsapp config types/schema and implements account-level resolution and group prompt composition; the final injected prompt is the account prompt plus the resolved group prompt (joined with a blank line). The PR includes tests and docs and limits changes to WhatsApp gateway/config handling.", - "maintainer_signal_summary": "Backwards-compatible feature change touching gateway/orchestration, integrations and API/contracts; includes tests and documentation, does not change non-WhatsApp channels, hot-reload behavior, or group allowlist/mention-gating semantics, and closes the linked issue.", - "dedupe_summary": "Extend WhatsApp config to support systemPrompt at channels.whatsapp, channels.whatsapp.accounts., and channels.whatsapp(.accounts).groups. with resolution: account systemPrompt overrides root systemPrompt; group resolution prefers specific group then '*' wildcard within the active groups map; account-level groups fully replace root groups; final prompt = account_prompt + blank line + resolved_group_prompt." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of adding multi-level WhatsApp systemPrompt support and the precise resolution rules, which is ideal for deduplication." - }, - "usage": { - "input_tokens": 2410, - "output_tokens": 633 - } - }, - { - "thread_id": 17692, - "number": 45354, - "kind": "pull_request", - "title": "Fix WhatsApp group append processing and account-scoped gatin", - "summary": { - "problem_summary": "WhatsApp group messages received via messages.upsert(type=\"append\") were being skipped before entering the inbound/agent path, and WhatsApp group policy resolution could ignore account-scoped configuration (accountId), causing account-scoped groupAllowFrom/allowFrom to be overlooked and allowlist gating to fail closed.", - "solution_summary": "Append processing was enabled only for WhatsApp group messages (DM append history still skipped) and group policy resolution was changed to propagate accountId and use account-scoped groupAllowFrom/allowFrom fallback when resolving WhatsApp group gating defaults.", - "maintainer_signal_summary": "Bug fix limited to WhatsApp integration; no config/schema changes. Touches src/web/inbound/monitor.ts and src/web/auto-reply/monitor/group-activation.ts; verified against live deployment and targeted tests. Risk: group append may process backlog messages (mitigated by scoping to groups and keeping DM append skipped).", - "dedupe_summary": "Fix: WhatsApp group messages delivered as messages.upsert(type=\"append\") were dropped and account-scoped WhatsApp group policy ignored; change scopes append processing to groups and propagates accountId so account-scoped groupAllowFrom/allowFrom are applied during group gating." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core bug (append-typed group messages dropped) and the fix (limit append processing to groups and use accountId so account-scoped groupAllowFrom/allowFrom are honored), with no template noise." - }, - "usage": { - "input_tokens": 1962, - "output_tokens": 715 - } - }, - { - "thread_id": 20932, - "number": 52512, - "kind": "pull_request", - "title": "fix(gateway): resolve Canvas 401 behind Tailscale Serve", - "summary": { - "problem_summary": "Canvas/A2UI returned 401 when accessed via Tailscale Serve because the gateway mis-identified the request origin: resolveClientIp returned undefined when a trusted proxy connected without X-Forwarded-* headers, and isLocalDirectRequest did not fall back to the socket remoteAddress when XFF resolved to a non-loopback Tailscale IP.", - "solution_summary": "Make resolveClientIp return the socket address as a fallback for direct trusted-proxy requests; update isLocalDirectRequest to consult req.socket.remoteAddress when auth.allowRealIpFallback is enabled; gate .ts.net host acceptance behind the existing auth.allowTailscale flag and thread that flag through callers; update docs and add tests.", - "maintainer_signal_summary": "Closes #28472 and depends on #50636; tests added/updated (src/gateway/auth.test.ts, net.test.ts) and passing; no new config keys introduced but docs require gateway.trustedProxies and gateway.allowRealIpFallback when using Tailscale Serve; small behavioral risk limited to trusted-proxy no-XFF cases.", - "dedupe_summary": "Fix 401 by having resolveClientIp fall back to req.socket.remoteAddress for trusted proxies that omit forwarding headers, and by changing isLocalDirectRequest to use socket remoteAddress when allowRealIpFallback is true; additionally gate .ts.net host-header acceptance behind auth.allowTailscale and thread that flag through gateway auth/net/server components." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that captures the core code changes (resolveClientIp and isLocalDirectRequest fallbacks, .ts.net gating, and threading auth.allowTailscale) without boilerplate or irrelevant details." - }, - "usage": { - "input_tokens": 1801, - "output_tokens": 796 - } - }, - { - "thread_id": 13791, - "number": 42906, - "kind": "pull_request", - "title": "fix(security): block non-self messages in WhatsApp self-chat mode", - "summary": { - "problem_summary": "In WhatsApp self-chat deployments, checkInboundAccessControl() computed an isSelfChat heuristic but never used it as a gate, so non-self messages (group messages, DMs from strangers, broadcasts) could pass through because resolveWhatsAppRuntimeGroupPolicy() defaults to \"open\" when groupPolicy is unspecified. This violates the intended self-chat isolation boundary in the inbound access-control path.", - "solution_summary": "Added an early-return guard in src/web/inbound/access-control.ts: when account.selfChatMode === true, non-self messages are immediately rejected before any group/DM policy evaluation, preserving self-messages via isSamePhone and retaining the isSelfChat value for logging/UI while avoiding heuristic-based blocking.", - "maintainer_signal_summary": "Change is scoped to access-control.ts for WhatsApp inbound processing; it intentionally only enforces explicit selfChatMode (auto-detected heuristic is not hard-blocked) and preserves existing group/DM policy resolution for other cases. All relevant tests pass and the guard can be reverted by removing the early-return block.", - "dedupe_summary": "Fix: enforce explicit WhatsApp self-chat isolation by adding an early-return in checkInboundAccessControl to block all non-self messages when account.selfChatMode === true; addresses a bug where isSelfChat was computed but not enforced and groupPolicy defaulted to \"open\", allowing unauthorized non-self messages through." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that clearly states the bug and the fix, references the relevant functions/config (checkInboundAccessControl, account.selfChatMode, groupPolicy), and contains no template boilerplate." - }, - "usage": { - "input_tokens": 1516, - "output_tokens": 655 - } - }, - { - "thread_id": 4208, - "number": 4402, - "kind": "pull_request", - "title": "fix: store group messages from non-allowlisted senders as pending context", - "summary": { - "problem_summary": "With groupPolicy set to \"allowlist\", group messages from senders not in groupAllowFrom are currently dropped at the access-control layer and never reach the mention-gating/context-storage logic, so the agent cannot see non-allowlisted members' messages as pending context.", - "solution_summary": "Introduce an opt-in flag groupContextFromAll (default false) that causes access-control to return storeForContext for non-allowlisted group senders; the inbound pipeline marks those messages as contextOnly (no read receipts, no reply triggered) and records them in group pending history. Added config/schema/types changes and threaded the new outcome through monitor/on-message to store context-only messages.", - "maintainer_signal_summary": "Backwards compatible (default false), opt-in per-account or top-level config, tested locally; changes are localized to WhatsApp inbound pipeline and config but Greptile flagged potential edge cases around phone-number normalization/allowlist matching and bypassing some group gating/state updates. Review should focus on allowlist normalization, interactions with applyGroupGating, and the new storeForContext/contextOnly handling across access-control and monitors.", - "dedupe_summary": "PR adds groupContextFromAll flag for WhatsApp to change behavior when groupPolicy: \"allowlist\": instead of dropping messages from non-allowlisted senders, access-control returns storeForContext and messages are marked contextOnly and stored in group pending history (no replies/read receipts); key files: src/web/inbound/access-control.ts, monitor.ts, on-message.ts, types/config schema." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: describes the new WhatsApp flag, the exact behavior change for groupPolicy: \"allowlist\" (store for context instead of drop, contextOnly, pending history), and thus will match similar issues; file paths are minor extra detail." - }, - "usage": { - "input_tokens": 1533, - "output_tokens": 590 - } - }, - { - "thread_id": 9553, - "number": 20072, - "kind": "pull_request", - "title": "feat(sessions_spawn): add sessionKey param to reuse sub-agent sessions", - "summary": { - "problem_summary": "sessions_spawn always created a new random-UUID session for sub-agents, preventing reuse of conversation history and enabling a session-key workflow that could bypass allowlist checks.", - "solution_summary": "Add an optional sessionKey parameter to sessions_spawn so sub-agents can run in deterministic sessions: short keys are namespaced to agent:{agentId}:subagent:{key}, fully-qualified keys are allowed but validated to ensure the embedded agentId matches targetAgentId, and omission falls back to crypto.randomUUID(); includes four tests and fixes allowlist bypass.", - "maintainer_signal_summary": "Blocking issues noted: a test imports a non-existent module (openclaw-tools.js vs clawdbot-tools.js) causing compile failure; cross-agent validation has a case-sensitivity bug (embedded agentId not normalized) causing false mismatches; inconsistent error handling (throw vs jsonResult) in security checks needs harmonization.", - "dedupe_summary": "Introduce sessionKey param to sessions_spawn to enable deterministic reuse of sub-agent sessions (namespacing short keys as agent:{agentId}:subagent:{key}, accepting fully-qualified keys only if their embedded agentId matches targetAgentId) and fall back to random UUID; addresses conversation-history reuse and allowlist bypass, but PR currently has a broken test import and case-sensitivity/error-handling validation bugs." - }, - "judge": { - "score": 4, - "has_boilerplate": true, - "signal_density": 4, - "would_cluster_correctly": true, - "reasoning": "Good, specific summary of the new sessionKey behavior and validation rules that will match similar issues, but it includes unnecessary test/import/bug notes that should be removed for deduplication." - }, - "usage": { - "input_tokens": 1586, - "output_tokens": 804 - } - }, - { - "thread_id": 8969, - "number": 21898, - "kind": "pull_request", - "title": "fix(telegram): auto-detect captionable messages for editMessageCaption", - "summary": { - "problem_summary": "Editing Telegram messages that contain media (photo, video, audio, document, animation) fails because the adapter calls editMessageText on caption-only media; Telegram responds 400 'there is no text in the message to edit', causing edits to fail and UI inconsistency.", - "solution_summary": "Updated src/telegram/send.ts editMessageTelegram to catch the specific 400 error (NO_TEXT_IN_MESSAGE_RE) and retry using editMessageCaption for captionable media; added a direct editMessageReplyMarkup path for buttons-only updates, preserved HTML parse fallback and MESSAGE_NOT_MODIFIED handling, and added six unit tests covering the paths.", - "maintainer_signal_summary": "Bug-fix PR scoped to the Telegram channel adapter only, backward compatible with no interface changes, adds two Telegram API methods and six tests; low risk but regex matching the exact Telegram error string is a potential fragility.", - "dedupe_summary": "In src/telegram/send.ts, editMessageTelegram now auto-detects captionable media by catching Telegram's 400 'there is no text in the message to edit' (NO_TEXT_IN_MESSAGE_RE) and retries with editMessageCaption; buttons-only updates use editMessageReplyMarkup; includes tests and preserves existing parse-mode and MESSAGE_NOT_MODIFIED behavior." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that pinpoints the exact code change and behavior (auto-retry to editMessageCaption on Telegram 400 for captionable media) with minimal noise, so it will cluster correctly with similar issues." - }, - "usage": { - "input_tokens": 1467, - "output_tokens": 725 - } - }, - { - "thread_id": 17568, - "number": 47391, - "kind": "pull_request", - "title": "fix(whatsapp): restore config-driven block streaming for WhatsApp delivery", - "summary": { - "problem_summary": "PR #24962 hardcoded disableBlockStreaming: true for the WhatsApp integration, which disabled block streaming entirely and caused all messages to be batched and sent only when the agent finished, ignoring per-account channels.whatsapp.blockStreaming and agents.defaults.blockStreamingDefault settings.", - "solution_summary": "Restore config-driven behavior: make disableBlockStreaming derive from the per-account channels.whatsapp.blockStreaming (falling back to agents.defaults.blockStreamingDefault) and change the deliver callback to suppress only tool payloads (reasoning payloads remain filtered upstream). Tests and live WhatsApp verification confirm progressive delivery at text boundaries.", - "maintainer_signal_summary": "Behavior is backward compatible; reasoning/thinking content is still suppressed upstream by shouldSuppressReasoningPayload; to disable this change quickly set channels.whatsapp.blockStreaming: false or remove the global default; watch for regressions if the upstream suppression filter is altered.", - "dedupe_summary": "WhatsApp integration bug: revert hardcoded disableBlockStreaming=true introduced in PR #24962 by making block streaming config-driven (channels.whatsapp.blockStreaming or agents.defaults.blockStreamingDefault) and update deliver callback to only suppress tool payloads while relying on shouldSuppressReasoningPayload upstream to block reasoning content." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change (undo hardcoded disableBlockStreaming, make it config-driven, and restrict deliver callback suppression to tool payloads) without boilerplate, so it would cluster with similar WhatsApp block-streaming fixes." - }, - "usage": { - "input_tokens": 1459, - "output_tokens": 575 - } - }, - { - "thread_id": 4129, - "number": 38337, - "kind": "pull_request", - "title": "fix(telegram): fix gateway hang on video messages and increase media download limit", - "summary": { - "problem_summary": "Sending video/media (>5MB) to the Telegram channel caused the gateway to hang indefinitely and become unresponsive because fetchRemoteMedia threw a content-length > maxBytes error and the dispatcher cleanup (Agent.close) deadlocked when the response body was left unconsumed.", - "solution_summary": "Increase default channels.telegram.mediaMaxMb from 5 to 20; fix closeDispatcher to avoid undici Agent.close deadlock by adding a timeout and fallback destroy; make media download errors non-fatal so captions/text still forward; add timeouts and retries to getFile and download flows; strip inbound media-path metadata from UI.", - "maintainer_signal_summary": "Backward-compatible default change (5→20MB configurable via channels.telegram.mediaMaxMb); verified end-to-end with a 7.5MB video and tests (strip-inbound-meta test added); no new permissions or dependencies; risk: 3s closeDispatcher timeout could trigger on slow drains but falls back to destroy and can be reverted in code.", - "dedupe_summary": "Fix Telegram gateway hang caused by undici Agent.close deadlock when response body is unconsumed by: raising telegram mediaMaxMb default to 20, making media download errors non-fatal, adding timeouts/retries to getFile/download, and changing closeDispatcher to use a timeout+destroy fallback." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, focused summary that names the root cause (undici Agent.close deadlock), the effect (Telegram gateway hang), and the specific fixes (increase mediaMaxMb, non-fatal media errors, timeouts/retries, timeout+destroy fallback) so it will match similar issues precisely." - }, - "usage": { - "input_tokens": 1555, - "output_tokens": 640 - } - }, - { - "thread_id": 21735, - "number": 55953, - "kind": "pull_request", - "title": "fix(telegram): use type-aware media placeholder for document attachments (#7116)", - "summary": { - "problem_summary": "When a Telegram user sends a document (PDF, DOCX, etc.) with no caption, the inbound body text fallback in the Telegram integration always used the hardcoded \"\", causing the agent to misidentify documents as images in resolveTelegramInboundBody.", - "solution_summary": "The fallback now uses the already-computed, type-aware placeholder variable so media placeholders resolve to the correct form (e.g. \"\", \"\", etc.) and unit tests were added to verify document placeholder behavior and getFile handling.", - "maintainer_signal_summary": "Change is scoped to agent-facing body text placeholder only; media download, extraction, and understanding pipelines are unchanged; new unit tests were added in extensions/telegram/src/bot/helpers.test.ts and extensions/telegram/src/bot.create-telegram-bot.test.ts; this closes #7116 and is backward compatible with low risk.", - "dedupe_summary": "Fix: replace hardcoded \"\" in resolveTelegramInboundBody fallback with the computed type-aware placeholder so non-audio Telegram attachments (e.g. documents) are labeled \"\"; unit tests added to lock in behavior." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the bug and fix naming the function and replacement placeholder (with an example) and only minor mention of tests, so it will cluster correctly with similar Telegram media-placeholder fixes." - }, - "usage": { - "input_tokens": 1382, - "output_tokens": 604 - } - }, - { - "thread_id": 2463, - "number": 40950, - "kind": "pull_request", - "title": "media: make MEDIA_MAX_BYTES configurable via media.maxBytes", - "summary": { - "problem_summary": "Media staging limit is hardcoded to MEDIA_MAX_BYTES (5 MB) in src/media/store.ts, so files larger than 5 MB silently fail to stage into sandboxed agents and users cannot override the cap via config.", - "solution_summary": "Adds a new media.maxBytes config field (optional, max 500 MB), replaces the hardcoded constant with getMediaMaxBytes()/setMediaMaxBytes(), has the gateway read and apply the config on boot, preserves default 5 MB when unset, and updates error messages to reflect the configured limit; tests updated and passing.", - "maintainer_signal_summary": "All relevant tests pass (56), type-check and lint clean; change is backward-compatible (default preserved, deprecated constant still exported), migration optional, schema caps value at 500 MB to mitigate disk-fill risk, and the PR closes #40880; reviewers’ bot comments were addressed.", - "dedupe_summary": "Make MEDIA_MAX_BYTES configurable: replace hardcoded 5MB in src/media/store.ts with get/set API driven by new media.maxBytes config applied at gateway boot (optional, default 5MB, schema max 500MB), and update error messaging to show the configured MB limit." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change (replace hardcoded 5MB with configurable media.maxBytes, applied at gateway boot, and updated error messaging) without boilerplate or irrelevant details." - }, - "usage": { - "input_tokens": 1503, - "output_tokens": 582 - } - }, - { - "thread_id": 5418, - "number": 30024, - "kind": "pull_request", - "title": "fix: pass file metadata to agent when Telegram media download fails", - "summary": { - "problem_summary": "Telegram integration: when a file/photo download failed (due to exceeding configured mediaMaxMb or Telegram API errors), the handler returned early and the entire message was silently dropped so the agent never saw the message, sent no ack, and the user got no indication.", - "solution_summary": "On download failure the code now extracts file metadata (name, size, type) from the Telegram message object, builds a metadata stub injected into msg.text explaining why the file couldn't be downloaded, and allows the message to continue through the normal pipeline so the agent can handle/respond.", - "maintainer_signal_summary": "Bug fix in src/telegram/bot-handlers.ts affecting integrations; backward compatible, no config changes, removes prior automatic \"file too large/failed to download\" bot replies so agent decides response; revertable by reverting the single commit; risk limited to error-path mutation of msg.text.", - "dedupe_summary": "Fix for Telegram handler early-return on media download failure: instead of returning on size/API errors (e.g., >mediaMaxMb default 5MB or Telegram 20MB getFile limit), build a file-metadata stub (name,size,type,reason) and append to msg.text then continue to processMessage so files aren't silently dropped." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and focused: it states the bug (early-return on Telegram media download failure causing silent drops) and the exact fix (build/append file-metadata stub and continue processing), so it will correctly cluster similar reports." - }, - "usage": { - "input_tokens": 1345, - "output_tokens": 648 - } - }, - { - "thread_id": 5796, - "number": 24117, - "kind": "pull_request", - "title": "feat(cli): add --session-key targeting for openclaw agent", - "summary": { - "problem_summary": "openclaw agent CLI lacked a --session-key option, causing CLI-triggered deliveries to resolve to the agent main session when external automation had a stable session key but not a UUID sessionId (making callback routing unreliable).", - "solution_summary": "Adds a --session-key CLI flag in src/cli/program/register.agent.ts, extends AgentCliOpts with sessionKey and forwards it into resolveSessionKeyForRequest via src/commands/agent-via-gateway.ts, updates help/validation text, and adds tests for parsing and gateway forwarding.", - "maintainer_signal_summary": "Includes unit tests (register.agent.test.ts and agent-via-gateway.test.ts) that pass locally; change is additive/opt-in, backward compatible, and presented as low-risk.", - "dedupe_summary": "Add --session-key flag to openclaw agent CLI to target stable session keys (e.g., agent:demo:slack:channel:...), forward sessionKey into resolveSessionKeyForRequest and gateway calls, preserve resolution precedence (sessionKey > sessionId > to > agent), and update help/validation with tests." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that highlights the new --session-key CLI flag, where it's propagated (resolveSessionKeyForRequest/gateway), and the preserved resolution precedence, which is sufficient for deduplication." - }, - "usage": { - "input_tokens": 1317, - "output_tokens": 809 - } - }, - { - "thread_id": 766, - "number": 11611, - "kind": "pull_request", - "title": "feat: separate group-level allowlist from sender-level command authorization", - "summary": { - "problem_summary": "groupPolicy: \"allowlist\" currently uses groupAllowFrom (sender phone numbers) to gate all group interactions, so you cannot allow specific group JIDs while letting any participant chat, nor separate slash-command authorization from group-level acceptance, and spam groups cannot be blocked by JID.", - "solution_summary": "When groupPolicy is \"allowlist\" and a channels.whatsapp.groups mapping exists, inbound access control now gates by group JID (the groups mapping) instead of sender phone number; groupAllowFrom remains the gate for slash commands, anyone in an approved group can chat, and when no groups mapping exists the legacy sender-based behavior is preserved. The change updates src/web/inbound/access-control.ts, docs in src/config/types.whatsapp.ts, and adds four tests covering the new behavior (depends on #3326).", - "maintainer_signal_summary": "Localized change to checkInboundAccessControl delegating to resolveChannelGroupPolicy; backward-compatible fallback preserved; JSDoc updated and four Vitest cases added; one minor outdated inline comment noted but logic appears safe.", - "dedupe_summary": "Change WhatsApp group allowlist behavior so that when channels.whatsapp.groups (group JID mapping) is present and groupPolicy=\"allowlist\", inbound group access is determined by group JIDs (allowlisted groups) while groupAllowFrom continues to control slash-command authorization; if no groups mapping exists, fall back to sender-based filtering." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it captures the change to use channels.whatsapp.groups (group JIDs) as the allowlist when groupPolicy=\"allowlist\", preserves groupAllowFrom for command auth, and notes the sender-based fallback." - }, - "usage": { - "input_tokens": 1367, - "output_tokens": 672 - } - }, - { - "thread_id": 17924, - "number": 46425, - "kind": "pull_request", - "title": "fix(telegram): propagate mediaMaxMb limit to outbound media reply delivery", - "summary": { - "problem_summary": "Telegram account setting mediaMaxMb (mediaMaxBytes) was not being applied for outbound reply messages; loadWebMedia in the reply delivery path received no maxBytes and fell back to the 100 MB default, so replies could exceed operator-configured limits.", - "solution_summary": "Thread mediaMaxBytes through the Telegram outbound delivery chain (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts) and the native-commands path (bot-native-commands.ts) so loadWebMedia receives the configured maxBytes; added unit tests verifying behavior and preserved behavior when unset.", - "maintainer_signal_summary": "This is a small bugfix touching Telegram integration; changes are backward compatible and limited to parameter threading, with two unit tests added and 108 related tests passing; no defaults or media internals changed and the direct-send path was already correct.", - "dedupe_summary": "Thread mediaMaxBytes parameter to loadWebMedia for Telegram outbound reply delivery (bot.ts → bot-message.ts → bot-message-dispatch.ts → delivery.replies.ts and bot-native-commands.ts) so mediaMaxMb configured on telegramAccounts is enforced instead of falling back to the 100MB default." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core fix (threading mediaMaxBytes through listed files to enforce telegramAccounts' mediaMaxMb instead of the 100MB default) with no boilerplate or noise." - }, - "usage": { - "input_tokens": 1368, - "output_tokens": 437 - } - }, - { - "thread_id": 5712, - "number": 37050, - "kind": "pull_request", - "title": "fix(telegram): add HTML parse fallback for media captions", - "summary": { - "problem_summary": "Media sends with captions containing unmatched or invalid HTML entities caused Telegram API 400 \"can't parse entities\" errors and the entire media send would fail because only text messages had an HTML-parse fallback; the media send path lacked retry logic.", - "solution_summary": "Wrap the media send callback in withTelegramHtmlParseFallback so that on an HTML parse error the media send is retried without parse_mode and with the raw caption; this change covers photo, document, video, animation, audio, voice and video note sends and includes a unit test verifying the retry.", - "maintainer_signal_summary": "Change is limited to the media send inner callback in src/telegram/send.ts; text message, edit message, thread fallback and caption splitting logic are unchanged. A unit test was added to confirm the first call uses parse_mode: 'HTML' and the retry omits parse_mode and uses the raw caption; backwards compatible and small scope.", - "dedupe_summary": "Add HTML parse fallback for Telegram media captions by wrapping media send callbacks (sendPhoto, sendDocument, sendVideo, sendAnimation, sendAudio, sendVoice, video_note) in withTelegramHtmlParseFallback so parse errors (\"can't parse entities\") are retried without parse_mode using the raw caption (implemented in src/telegram/send.ts)." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: describes adding an HTML-parse fallback for Telegram media captions by wrapping media send functions in withTelegramHtmlParseFallback and retrying without parse_mode using the raw caption, which is exactly the core change." - }, - "usage": { - "input_tokens": 1242, - "output_tokens": 828 - } - }, - { - "thread_id": 21769, - "number": 55280, - "kind": "pull_request", - "title": "fix(whatsapp): make block streaming configurable and fix delivery race in block-reply pipeline", - "summary": { - "problem_summary": "WhatsApp had block streaming effectively disabled/hardcoded which batched intermediate text and introduced a delivery race in the block-reply pipeline, sometimes causing neither intermediate blocks nor the final reply to be delivered.", - "solution_summary": "Make block streaming configurable via channels.whatsapp.blockStreaming (per-account/global), fix the boolean handling so the setting actually activates, add coalescing defaults, implement dispatcher.sendBlockReplyAsync and await block delivery in onBlockReply so sentContentKeys are marked only after confirmed delivery, and update tests.", - "maintainer_signal_summary": "Existing tests were updated and new cases added; manual verification shows streaming-off yields a single final reply and streaming-on delivers incremental blocks. This is a root-cause fix (not a revert) addressing the optimistic-enqueue race by awaiting confirmed delivery.", - "dedupe_summary": "Enable configurable WhatsApp block streaming and fix delivery race by replacing optimistic dispatcher.sendBlockReply() enqueue-without-await with sendBlockReplyAsync awaited in onBlockReply, correct disableBlockStreaming boolean logic so blockStreaming actually activates, add blockStreamingCoalesceDefaults, and adjust dispatch/pipeline to mark sentContentKeys only after confirmed delivery." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, technical summary that captures the core change—making WhatsApp block streaming configurable and fixing the delivery race by awaiting block-reply sends and correcting the boolean logic—without boilerplate, so it will cluster with similar fixes." - }, - "usage": { - "input_tokens": 1155, - "output_tokens": 611 - } - }, - { - "thread_id": 8098, - "number": 25543, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked group-level allowlist support — only sender-level groupAllowFrom existed, so specific Signal groups could not be explicitly allowed to bypass sender checks or run control commands.", - "solution_summary": "Introduces channels.signal.groups. config with a new SignalGroupConfig type and Zod schema, wires resolveChannelGroupPolicy into the Signal event handler and command authorization so configured groups bypass sender-level checks, and updates docs.", - "maintainer_signal_summary": "TypeScript compilation passes and manual Signal testing performed; implementation follows existing patterns from other channels, preserves compatibility with groupAllowFrom, and integrates group-level allow into commandGate; minimal-risk change with docs added.", - "dedupe_summary": "Add group-level allowlist for Signal via channels.signal.groups. config (SignalGroupConfig + Zod schema) that lets listed groups bypass sender-level groupAllowFrom and be authorized for control commands by wiring resolveChannelGroupPolicy into event handling and command authorization." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary of the core change (Signal group-level allowlist config, schema and event/authorization wiring) without boilerplate, so it will cluster with similar Signal group-allowlist/authorization changes." - }, - "usage": { - "input_tokens": 1097, - "output_tokens": 491 - } - }, - { - "thread_id": 539, - "number": 29154, - "kind": "pull_request", - "title": "fix(signal): add group-level allowlist support via groups config", - "summary": { - "problem_summary": "Signal channel lacked group-level allowlisting via channels.signal.groups., so when groupPolicy was set to \"allowlist\" and groupAllowFrom contained group IDs, group messages were silently dropped because groupAllowFrom is a sender-level filter (sender phone/UUID never matches a group ID).", - "solution_summary": "Adds a groups config to SignalAccountSchemaBase and invokes resolveChannelGroupPolicy in Signal's group access check before falling back to sender-level groupAllowFrom; adds four tests to cover group-policy resolution and includes an example config.", - "maintainer_signal_summary": "Schema change: adds channels.signal.groups (requireMention, tools, toolsBySender). Event handler change: insert resolveChannelGroupPolicy call for Signal. Tests: +4 tests, all existing config/group-policy/monitor tests pass; fixes #25540.", - "dedupe_summary": "Add group-level allowlist support for Signal by adding channels.signal.groups to the schema and calling resolveChannelGroupPolicy in Signal's access check so group IDs in groupPolicy: \"allowlist\" are recognized instead of being misinterpreted by sender-level groupAllowFrom." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise and specific: it captures the missing Signal group-level allowlist, explains the root cause (group IDs misinterpreted by sender-level groupAllowFrom), and states the code changes (add channels.signal.groups and call resolveChannelGroupPolicy), so it will cluster correctly with similar fixes." - }, - "usage": { - "input_tokens": 897, - "output_tokens": 620 - } - }, - { - "thread_id": 5565, - "number": 28178, - "kind": "pull_request", - "title": "feat(whatsapp): support config-driven block streaming", - "summary": { - "problem_summary": "WhatsApp messages are currently coalesced into one final payload when an agent sends multiple messages with tool calls/sleep between them because process-message.ts hardcodes disableBlockStreaming: true and uses queuedFinal to mark delivery, preventing intermediate blocks from being sent as separate replies.", - "solution_summary": "This PR makes WhatsApp respect the channels.whatsapp.blockStreaming config by adding resolveWhatsAppBlockStreamingEnabled(), replacing the hardcoded disableBlockStreaming, fixing the queuedFinal/delivery logic so intermediate blocks count as replies when blockStreaming is enabled, registering blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE in the WhatsApp dock, and updating tests.", - "maintainer_signal_summary": "Includes code changes in process-message.ts and dock.ts, updated inbound contract tests to cover enabled/disabled/default behaviors, all 10 existing tests pass; labels: channel: whatsapp-web, app: web-ui, agents, size: S.", - "dedupe_summary": "Make WhatsApp channel respect config-driven blockStreaming (channels.whatsapp.blockStreaming) instead of hardcoded disableBlockStreaming:true; fix delivery logic so intermediate block payloads are treated as replies; register blockStreaming capability and DEFAULT_BLOCK_STREAMING_COALESCE; update tests." - }, - "judge": { - "score": 5, - "has_boilerplate": false, - "signal_density": 5, - "would_cluster_correctly": true, - "reasoning": "Concise, specific summary that captures the core change (config-driven blockStreaming for WhatsApp), the delivery-fix for intermediate block payloads, and capability registration without boilerplate or irrelevant details." - }, - "usage": { - "input_tokens": 647, - "output_tokens": 573 - } - } - ] -} \ No newline at end of file diff --git a/.context/compound-engineering/ce-optimize/summary-prompt/test-threads.yaml b/.context/compound-engineering/ce-optimize/summary-prompt/test-threads.yaml deleted file mode 100644 index 6e194d7..0000000 --- a/.context/compound-engineering/ce-optimize/summary-prompt/test-threads.yaml +++ /dev/null @@ -1,92 +0,0 @@ -spec_name: summary-prompt -description: > - Optimize the system prompt for summarizing GitHub issues/PRs. - Goal: extract signal, strip template boilerplate, remove testing notes. - Judge: LLM rates summary quality for deduplication clustering. - -test_threads: - issues: - - { thread_id: 15126, number: 49990, cluster_id: 245465, cluster_size: 10, body_length: 7604 } - - { thread_id: 8920, number: 30244, cluster_id: 245465, cluster_size: 10, body_length: 3390 } - - { thread_id: 19616, number: 51018, cluster_id: 245465, cluster_size: 10, body_length: 3358 } - - { thread_id: 16324, number: 47670, cluster_id: 245465, cluster_size: 10, body_length: 3025 } - - { thread_id: 10106, number: 28788, cluster_id: 245465, cluster_size: 10, body_length: 2772 } - - { thread_id: 14855, number: 44448, cluster_id: 245467, cluster_size: 10, body_length: 2554 } - - { thread_id: 18179, number: 44584, cluster_id: 245467, cluster_size: 10, body_length: 2413 } - - { thread_id: 2538, number: 40880, cluster_id: 245464, cluster_size: 10, body_length: 2392 } - - { thread_id: 9401, number: 27409, cluster_id: 245465, cluster_size: 10, body_length: 2275 } - - { thread_id: 9156, number: 30221, cluster_id: 245465, cluster_size: 10, body_length: 2249 } - - { thread_id: 18848, number: 44582, cluster_id: 245467, cluster_size: 10, body_length: 2143 } - - { thread_id: 14856, number: 44447, cluster_id: 245467, cluster_size: 10, body_length: 2126 } - - { thread_id: 14863, number: 44449, cluster_id: 245467, cluster_size: 10, body_length: 2038 } - - { thread_id: 18847, number: 44586, cluster_id: 245467, cluster_size: 10, body_length: 2000 } - - { thread_id: 5022, number: 37344, cluster_id: 245465, cluster_size: 10, body_length: 1891 } - - { thread_id: 14862, number: 44450, cluster_id: 245467, cluster_size: 10, body_length: 1850 } - - { thread_id: 14859, number: 44453, cluster_id: 245467, cluster_size: 10, body_length: 1780 } - - { thread_id: 14142, number: 42702, cluster_id: 245465, cluster_size: 10, body_length: 1776 } - - { thread_id: 14861, number: 44451, cluster_id: 245467, cluster_size: 10, body_length: 1703 } - - { thread_id: 21902, number: 55855, cluster_id: 245466, cluster_size: 10, body_length: 1679 } - - pull_requests: - - { thread_id: 22366, number: 55328, cluster_id: 245463, cluster_size: 10, body_length: 10165 } - - { thread_id: 17692, number: 45354, cluster_id: 245463, cluster_size: 10, body_length: 7728 } - - { thread_id: 20932, number: 52512, cluster_id: 245462, cluster_size: 11, body_length: 6111 } - - { thread_id: 13791, number: 42906, cluster_id: 245463, cluster_size: 10, body_length: 5610 } - - { thread_id: 4208, number: 4402, cluster_id: 245463, cluster_size: 10, body_length: 5532 } - - { thread_id: 9553, number: 20072, cluster_id: 245468, cluster_size: 9, body_length: 5490 } - - { thread_id: 8969, number: 21898, cluster_id: 245464, cluster_size: 10, body_length: 5434 } - - { thread_id: 17568, number: 47391, cluster_id: 245463, cluster_size: 10, body_length: 5429 } - - { thread_id: 4129, number: 38337, cluster_id: 245464, cluster_size: 10, body_length: 5407 } - - { thread_id: 21735, number: 55953, cluster_id: 245464, cluster_size: 10, body_length: 5162 } - - { thread_id: 2463, number: 40950, cluster_id: 245464, cluster_size: 10, body_length: 4839 } - - { thread_id: 5418, number: 30024, cluster_id: 245464, cluster_size: 10, body_length: 4622 } - - { thread_id: 5796, number: 24117, cluster_id: 245468, cluster_size: 9, body_length: 4539 } - - { thread_id: 766, number: 11611, cluster_id: 245463, cluster_size: 10, body_length: 4483 } - - { thread_id: 17924, number: 46425, cluster_id: 245464, cluster_size: 10, body_length: 4429 } - - { thread_id: 5712, number: 37050, cluster_id: 245464, cluster_size: 10, body_length: 4387 } - - { thread_id: 21769, number: 55280, cluster_id: 245463, cluster_size: 10, body_length: 3817 } - - { thread_id: 8098, number: 25543, cluster_id: 245463, cluster_size: 10, body_length: 3514 } - - { thread_id: 539, number: 29154, cluster_id: 245463, cluster_size: 10, body_length: 2620 } - - { thread_id: 5565, number: 28178, cluster_id: 245463, cluster_size: 10, body_length: 1698 } - -thread_ids_flat: - - 15126 - - 8920 - - 19616 - - 16324 - - 10106 - - 14855 - - 18179 - - 2538 - - 9401 - - 9156 - - 18848 - - 14856 - - 14863 - - 18847 - - 5022 - - 14862 - - 14859 - - 14142 - - 14861 - - 21902 - - 22366 - - 17692 - - 20932 - - 13791 - - 4208 - - 9553 - - 8969 - - 17568 - - 4129 - - 21735 - - 2463 - - 5418 - - 5796 - - 766 - - 17924 - - 5712 - - 21769 - - 8098 - - 539 - - 5565 diff --git a/.context/compound-engineering/ce-optimize/vectorlite-clustering/experiment-log.yaml b/.context/compound-engineering/ce-optimize/vectorlite-clustering/experiment-log.yaml deleted file mode 100644 index 81246db..0000000 --- a/.context/compound-engineering/ce-optimize/vectorlite-clustering/experiment-log.yaml +++ /dev/null @@ -1,225 +0,0 @@ -spec_name: vectorlite-clustering -started_at: "2026-03-29T00:00:00Z" -goal: > - Replace in-memory exact kNN with vectorlite ANN + post-processing. - Target: >=50% cluster membership (threads in multi-member clusters / total threads). - Constraint: no mega-clusters (max cluster size capped). - -baseline: - timestamp: "2026-03-29" - params: { backend: vectorlite, k: 6, min_score: 0.82, candidate_k: 96, cluster_mode: basic } - metrics: - multi_member_pct: 31.37 - edge_count: 6934 - cluster_count: 14338 - solo_clusters: 12708 - threads_in_multi: 5808 - total_threads: 18516 - max_cluster_size: 619 - avg_multi_size: 3.56 - duration_ms: 129190 - -experiments: - - id: 1 - description: "Lower threshold to 0.75 (no refinement)" - params: { k: 6, min_score: 0.75, cluster_mode: basic } - outcome: degenerate - metrics: - multi_member_pct: 68.64 - edge_count: 29157 - solo_clusters: 5807 - max_cluster_size: 9780 # mega-cluster — 53% of all threads - notes: "Union-Find transitive closure creates one giant blob" - - - id: 2 - description: "Threshold 0.78 (no refinement)" - params: { k: 6, min_score: 0.78, cluster_mode: basic } - outcome: degenerate - metrics: - multi_member_pct: 53.23 - edge_count: 16400 - solo_clusters: 8660 - max_cluster_size: 4905 - - - id: 3 - description: "Threshold 0.80 (no refinement)" - params: { k: 6, min_score: 0.80, cluster_mode: basic } - outcome: degenerate - metrics: - multi_member_pct: 42.0 - edge_count: 10744 - solo_clusters: 10739 - max_cluster_size: 2417 - - - id: 4 - description: "Threshold 0.78 with refine mode (max=100, step=0.02)" - params: { k: 6, min_score: 0.78, cluster_mode: refine, max_cluster_size: 100, refine_step: 0.02 } - outcome: improved - metrics: - multi_member_pct: 44.58 - edge_count: 16400 - solo_clusters: 10262 - max_cluster_size: 89 - avg_multi_size: 3.77 - notes: "Refinement works but creates extra solos by splitting mega-clusters with higher thresholds" - - - id: 5 - description: "t=0.75, k=10, refine (max=100, step=0.03)" - params: { k: 10, min_score: 0.75, cluster_mode: refine, max_cluster_size: 100, refine_step: 0.03 } - outcome: improved - metrics: - multi_member_pct: 47.0 - edge_count: 34913 - solo_clusters: 9814 - max_cluster_size: 94 - avg_multi_size: 3.66 - - - id: 6 - description: "t=0.75, k=10, refine (max=200, step=0.01)" - params: { k: 10, min_score: 0.75, cluster_mode: refine, max_cluster_size: 200, refine_step: 0.01 } - outcome: kept - metrics: - multi_member_pct: 52.2 - edge_count: 34913 - solo_clusters: 8850 - max_cluster_size: 164 - avg_multi_size: 4.11 - duration_ms: 156806 - cluster_build_ms: 575 - notes: "First experiment to hit 50% target" - - - id: 7 - description: "t=0.73, k=12, refine (max=200, step=0.01)" - params: { k: 12, min_score: 0.73, cluster_mode: refine, max_cluster_size: 200, refine_step: 0.01 } - outcome: improved - metrics: - multi_member_pct: 54.48 - edge_count: 54496 - solo_clusters: 8429 - max_cluster_size: 164 - avg_multi_size: 4.1 - - - id: 8 - description: "t=0.73, k=10, refine (max=200, step=0.01)" - params: { k: 10, min_score: 0.73, cluster_mode: refine, max_cluster_size: 200, refine_step: 0.01 } - outcome: improved - metrics: - multi_member_pct: 54.48 - edge_count: 50621 - solo_clusters: 8429 - max_cluster_size: 164 - avg_multi_size: 4.1 - - - id: 9 - description: "t=0.75, k=10, candidateK=200, refine (max=200, step=0.01)" - params: { k: 10, min_score: 0.75, candidate_k: 200, cluster_mode: refine, max_cluster_size: 200, refine_step: 0.01 } - outcome: no_improvement - metrics: - multi_member_pct: 52.2 - edge_count: 34913 - solo_clusters: 8850 - max_cluster_size: 164 - notes: "Higher candidateK did not help — ANN recall already sufficient" - - - id: 10 - description: "t=0.73, k=15, efSearch=300, refine (max=200, step=0.01)" - params: { k: 15, min_score: 0.73, ef_search: 300, cluster_mode: refine, max_cluster_size: 200, refine_step: 0.01 } - outcome: no_improvement - metrics: - multi_member_pct: 54.48 - edge_count: 59059 - solo_clusters: 8429 - max_cluster_size: 164 - notes: "More edges from higher k/efSearch but same membership — solos have NO neighbors above threshold" - - # --- Size-bounded Union-Find experiments (new algorithm) --- - - - id: 13 - description: "BOUNDED t=0.75, k=10, maxCluster=200" - params: { k: 10, min_score: 0.75, cluster_mode: bounded, max_cluster_size: 200 } - outcome: kept - metrics: - multi_member_pct: 64.47 - edge_count: 34913 - solo_clusters: 6579 - threads_in_multi: 11937 - max_cluster_size: 200 - avg_multi_size: 8.02 - cluster_build_ms: 30 - notes: "Dramatically better than refine — processes edges strongest-first, caps cluster size" - - - id: 14 - description: "BOUNDED t=0.75, k=10, maxCluster=100" - params: { k: 10, min_score: 0.75, cluster_mode: bounded, max_cluster_size: 100 } - outcome: kept - metrics: - multi_member_pct: 64.83 - edge_count: 34913 - solo_clusters: 6513 - threads_in_multi: 12003 - max_cluster_size: 100 - avg_multi_size: 7.89 - - - id: 15 - description: "BOUNDED t=0.78, k=10, maxCluster=200" - params: { k: 10, min_score: 0.78, cluster_mode: bounded, max_cluster_size: 200 } - outcome: improved - metrics: - multi_member_pct: 51.69 - edge_count: 18448 - solo_clusters: 8945 - threads_in_multi: 9571 - max_cluster_size: 200 - avg_multi_size: 5.46 - - - id: 16 - description: "BOUNDED t=0.73, k=10, maxCluster=200" - params: { k: 10, min_score: 0.73, cluster_mode: bounded, max_cluster_size: 200 } - outcome: best - metrics: - multi_member_pct: 72.08 - edge_count: 50621 - solo_clusters: 5169 - threads_in_multi: 13347 - max_cluster_size: 200 - avg_multi_size: 10.53 - notes: "Best overall — 72% membership with controlled cluster sizes" - - - id: 17 - description: "BOUNDED t=0.75, k=6, maxCluster=200" - params: { k: 6, min_score: 0.75, cluster_mode: bounded, max_cluster_size: 200 } - outcome: kept - metrics: - multi_member_pct: 64.46 - edge_count: 29157 - solo_clusters: 6580 - threads_in_multi: 11936 - max_cluster_size: 200 - avg_multi_size: 8.05 - notes: "k=6 vs k=10 nearly identical — solo count is threshold-driven, not k-driven" - -key_learnings: - - "Union-Find transitive closure is the root cause of mega-clusters at lower thresholds" - - "Refine mode (raise threshold to split) creates extra solos — destructive approach" - - "Size-bounded Union-Find (greedy strongest-first with size cap) is far superior" - - "k and candidateK have minimal impact once threshold is set — solos have NO neighbors above threshold" - - "efSearch has no impact — ANN recall is already good at default candidateK" - - "The threshold is the primary lever for cluster membership percentage" - - "Bounded mode preserves 64-72% membership vs refine mode's 52-54% at same threshold" - -code_changes: - - file: packages/api-core/src/cluster/build.ts - changes: - - "Added buildSizeBoundedClusters() — greedy strongest-first Union-Find with size cap" - - "Added buildRefinedClusters() — iterative threshold-raising refinement (inferior)" - - "Made UnionFind.find() iterative to avoid stack overflow on deep chains" - - "Added unionBounded() with size tracking" - - "Refactored formatClusters() as shared helper" - - file: packages/api-core/src/service.ts - changes: - - "Added clusterMode, maxClusterSize, refineStep params to clusterExperiment()" - - "Imported buildRefinedClusters and buildSizeBoundedClusters" - - file: scripts/cluster-optimize-measure.mjs - changes: - - "New measurement harness script for optimization experiments" - - "Supports all clustering params including cluster-mode" From 1fc156637fb4cf2f3859fe51a7d87ed7b051940a Mon Sep 17 00:00:00 2001 From: huntharo Date: Fri, 3 Apr 2026 15:19:35 -0400 Subject: [PATCH 32/34] fix: prune stale vectorlite state --- packages/api-core/src/service.test.ts | 242 ++++++++++++++++++++++++++ packages/api-core/src/service.ts | 199 ++++++++++++++++----- 2 files changed, 397 insertions(+), 44 deletions(-) diff --git a/packages/api-core/src/service.test.ts b/packages/api-core/src/service.test.ts index 48d96b6..478553f 100644 --- a/packages/api-core/src/service.test.ts +++ b/packages/api-core/src/service.test.ts @@ -600,6 +600,99 @@ test('summarizeRepository includes hydrated human comments when includeComments } }); +test('summarizeRepository prices progress output using the configured summary model', async () => { + const progress: string[] = []; + const service = makeTestService( + { + checkAuth: async () => undefined, + getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }), + listRepositoryIssues: async () => [], + getIssue: async () => { + throw new Error('not expected'); + }, + getPull: async () => { + throw new Error('not expected'); + }, + listIssueComments: async () => [], + listPullReviews: async () => [], + listPullReviewComments: async () => [], + }, + { + checkAuth: async () => undefined, + summarizeThread: async () => ({ + summary: { + problemSummary: 'Problem', + solutionSummary: 'Solution', + maintainerSignalSummary: 'Signal', + dedupeSummary: 'Dedupe', + }, + usage: { + inputTokens: 1_000_000, + outputTokens: 0, + totalTokens: 1_000_000, + cachedInputTokens: 0, + reasoningTokens: 0, + }, + }), + embedTexts: async () => [], + }, + ); + + try { + const now = '2026-03-09T00:00:00Z'; + service.db + .prepare( + `insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at) + values (?, ?, ?, ?, ?, ?, ?)`, + ) + .run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now); + service.db + .prepare( + `insert into threads ( + id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url, + labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh, + merged_at_gh, first_pulled_at, last_pulled_at, updated_at + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ) + .run( + 10, + 1, + '100', + 42, + 'issue', + 'open', + 'Downloader hangs', + 'The transfer never finishes.', + 'alice', + 'User', + 'https://github.com/openclaw/openclaw/issues/42', + '["bug"]', + '[]', + '{}', + 'hash-42', + 0, + now, + now, + null, + null, + now, + now, + now, + ); + + await service.summarizeRepository({ + owner: 'openclaw', + repo: 'openclaw', + threadNumber: 42, + onProgress: (message) => progress.push(message), + }); + + assert.ok(progress.some((message) => message.includes('cost=$0.25') && message.includes('est_total=$0.25'))); + } finally { + service.close(); + } +}); + test('purgeComments removes hydrated comments and refreshes canonical documents', () => { const service = makeTestService({ checkAuth: async () => undefined, @@ -862,6 +955,84 @@ test('listNeighbors uses the vectorlite sidecar for current active vectors', asy } }); +test('embedRepository prunes closed vectors before reusing current active vectors', async () => { + const service = new GHCrawlService({ + config: makeTestConfig(), + github: { + checkAuth: async () => undefined, + getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }), + listRepositoryIssues: async () => [], + getIssue: async () => { + throw new Error('not expected'); + }, + getPull: async () => { + throw new Error('not expected'); + }, + listIssueComments: async () => [], + listPullReviews: async () => [], + listPullReviewComments: async () => [], + }, + ai: { + checkAuth: async () => undefined, + summarizeThread: async () => { + throw new Error('not expected'); + }, + embedTexts: async ({ texts }) => + texts.map((text) => { + if (text.includes('Target issue')) return makeEmbedding(1, 0); + if (text.includes('Closed similar one')) return makeEmbedding(0.999, 0.001); + if (text.includes('Closed similar two')) return makeEmbedding(0.998, 0.002); + if (text.includes('Open fallback')) return makeEmbedding(0.9, 0.1); + throw new Error(`unexpected embedding input: ${text}`); + }), + }, + }); + + try { + const now = '2026-03-09T00:00:00Z'; + service.db + .prepare( + `insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at) + values (?, ?, ?, ?, ?, ?, ?)`, + ) + .run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now); + const insertThread = service.db.prepare( + `insert into threads ( + id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url, + labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh, + merged_at_gh, first_pulled_at, last_pulled_at, updated_at + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ); + insertThread.run(10, 1, '100', 42, 'issue', 'open', 'Target issue', 'Primary issue body.', 'alice', 'User', 'https://github.com/openclaw/openclaw/issues/42', '[]', '[]', '{}', 'hash-42', 0, now, now, null, null, now, now, now); + insertThread.run(11, 1, '101', 43, 'issue', 'open', 'Closed similar one', 'Very similar body.', 'bob', 'User', 'https://github.com/openclaw/openclaw/issues/43', '[]', '[]', '{}', 'hash-43', 0, now, now, null, null, now, now, now); + insertThread.run(12, 1, '102', 44, 'issue', 'open', 'Closed similar two', 'Also very similar body.', 'carol', 'User', 'https://github.com/openclaw/openclaw/issues/44', '[]', '[]', '{}', 'hash-44', 0, now, now, null, null, now, now, now); + insertThread.run(13, 1, '103', 45, 'issue', 'open', 'Open fallback', 'Somewhat similar body.', 'dave', 'User', 'https://github.com/openclaw/openclaw/issues/45', '[]', '[]', '{}', 'hash-45', 0, now, now, null, null, now, now, now); + + await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); + + service.db + .prepare('update threads set state = ?, closed_at_gh = ?, updated_at = ? where id in (?, ?)') + .run('closed', now, now, 11, 12); + + const rerun = await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); + assert.equal(rerun.embedded, 0); + + const vectorCount = service.db.prepare('select count(*) as count from thread_vectors').get() as { count: number }; + assert.equal(vectorCount.count, 2); + + const result = service.listNeighbors({ + owner: 'openclaw', + repo: 'openclaw', + threadNumber: 42, + limit: 1, + minScore: 0.1, + }); + assert.deepEqual(result.neighbors.map((neighbor) => neighbor.number), [45]); + } finally { + service.close(); + } +}); + test('embedRepository truncates oversized inputs before submission', async () => { const embedCalls: string[][] = []; const service = new GHCrawlService({ @@ -1659,6 +1830,77 @@ test('clusterRepository purges legacy embeddings and inline vector payloads afte } }); +test('clusterExperiment falls back to active vectors when legacy embeddings are absent', async () => { + const service = new GHCrawlService({ + config: makeTestConfig(), + github: { + checkAuth: async () => undefined, + getRepo: async () => ({ id: 1, full_name: 'openclaw/openclaw' }), + listRepositoryIssues: async () => [], + getIssue: async () => { + throw new Error('not expected'); + }, + getPull: async () => { + throw new Error('not expected'); + }, + listIssueComments: async () => [], + listPullReviews: async () => [], + listPullReviewComments: async () => [], + }, + ai: { + checkAuth: async () => undefined, + summarizeThread: async () => { + throw new Error('not expected'); + }, + embedTexts: async ({ texts }) => texts.map((_text, index) => (index === 0 ? makeEmbedding(1, 0) : makeEmbedding(0.99, 0.01))), + }, + }); + + try { + const now = '2026-03-09T00:00:00Z'; + service.db + .prepare( + `insert into repositories (id, owner, name, full_name, github_repo_id, raw_json, updated_at) + values (?, ?, ?, ?, ?, ?, ?)`, + ) + .run(1, 'openclaw', 'openclaw', 'openclaw/openclaw', '1', '{}', now); + + const insertThread = service.db.prepare( + `insert into threads ( + id, repo_id, github_id, number, kind, state, title, body, author_login, author_type, html_url, + labels_json, assignees_json, raw_json, content_hash, is_draft, created_at_gh, updated_at_gh, closed_at_gh, + merged_at_gh, first_pulled_at, last_pulled_at, updated_at + ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + ); + insertThread.run(10, 1, '100', 42, 'issue', 'open', 'Downloader hangs', 'The transfer never finishes.', 'alice', 'User', 'https://github.com/openclaw/openclaw/issues/42', '[]', '[]', '{}', 'hash-42', 0, now, now, null, null, now, now, now); + insertThread.run(11, 1, '101', 43, 'issue', 'open', 'Downloader retry issue', 'The transfer retries forever.', 'bob', 'User', 'https://github.com/openclaw/openclaw/issues/43', '[]', '[]', '{}', 'hash-43', 0, now, now, null, null, now, now, now); + + await service.embedRepository({ owner: 'openclaw', repo: 'openclaw' }); + + const exact = service.clusterExperiment({ + owner: 'openclaw', + repo: 'openclaw', + backend: 'exact', + k: 1, + minScore: 0.5, + }); + const vectorlite = service.clusterExperiment({ + owner: 'openclaw', + repo: 'openclaw', + backend: 'vectorlite', + k: 1, + minScore: 0.5, + }); + + assert.equal(exact.threads, 2); + assert.equal(exact.clusters, 1); + assert.equal(vectorlite.threads, 2); + assert.equal(vectorlite.clusters, 1); + } finally { + service.close(); + } +}); + test('clusterRepository does not retain a parsed embedding cache in-process', async () => { const service = makeTestService({ checkAuth: async () => undefined, diff --git a/packages/api-core/src/service.ts b/packages/api-core/src/service.ts index ff79e15..5a8b45f 100644 --- a/packages/api-core/src/service.ts +++ b/packages/api-core/src/service.ts @@ -201,6 +201,13 @@ type ClusterExperimentResult = { clusterSizes: ClusterExperimentClusterSizeStats; clustersDetail: ClusterExperimentCluster[] | null; }; + +type SummaryModelPricing = { + inputCostPerM: number; + cachedInputCostPerM: number; + outputCostPerM: number; +}; + type EmbeddingWorkset = { rows: Array<{ id: number; @@ -367,6 +374,18 @@ const VECTORLITE_CLUSTER_EXPANDED_K = 24; const VECTORLITE_CLUSTER_EXPANDED_MULTIPLIER = 4; const VECTORLITE_CLUSTER_EXPANDED_CANDIDATE_K = 512; const VECTORLITE_CLUSTER_EXPANDED_EF_SEARCH = 1024; +const SUMMARY_MODEL_PRICING: Record = { + 'gpt-5-mini': { + inputCostPerM: 0.25, + cachedInputCostPerM: 0.025, + outputCostPerM: 2.0, + }, + 'gpt-5.4-mini': { + inputCostPerM: 0.75, + cachedInputCostPerM: 0.075, + outputCostPerM: 4.5, + }, +}; function nowIso(): string { return new Date().toISOString(); @@ -1066,10 +1085,7 @@ export class GHCrawlService { let cachedInputTokens = 0; const startTime = Date.now(); - // gpt-5.4-mini pricing per million tokens - const INPUT_COST_PER_M = 0.75; - const CACHED_INPUT_COST_PER_M = 0.075; - const OUTPUT_COST_PER_M = 4.50; + const pricing = SUMMARY_MODEL_PRICING[this.config.summaryModel] ?? null; // Stage 1: concurrent API calls const fetcher = new IterableMapper( @@ -1110,20 +1126,10 @@ export class GHCrawlService { // Compute cost and ETA every 10 items or on the last item if (index % 10 === 0 || index === pending.length) { - const uncachedInput = inputTokens - cachedInputTokens; - const costSoFar = - (uncachedInput / 1_000_000) * INPUT_COST_PER_M + - (cachedInputTokens / 1_000_000) * CACHED_INPUT_COST_PER_M + - (outputTokens / 1_000_000) * OUTPUT_COST_PER_M; const remaining = pending.length - index; const avgIn = inputTokens / index; const avgOut = outputTokens / index; const avgCachedIn = cachedInputTokens / index; - const estTotalCost = - costSoFar + - ((remaining * (avgIn - avgCachedIn)) / 1_000_000) * INPUT_COST_PER_M + - ((remaining * avgCachedIn) / 1_000_000) * CACHED_INPUT_COST_PER_M + - ((remaining * avgOut) / 1_000_000) * OUTPUT_COST_PER_M; const elapsedSec = (Date.now() - startTime) / 1000; const secPerItem = elapsedSec / index; @@ -1131,9 +1137,25 @@ export class GHCrawlService { const etaMin = Math.round(etaSec / 60); const etaStr = etaMin >= 60 ? `${Math.floor(etaMin / 60)}h${etaMin % 60}m` : `${etaMin}m`; - params.onProgress?.( - `[summarize] ${index}/${pending.length} thread #${row.number} | cost=$${costSoFar.toFixed(2)} est_total=$${estTotalCost.toFixed(2)} | avg_in=${Math.round(avgIn)} avg_out=${Math.round(avgOut)} | ETA ${etaStr}`, - ); + if (pricing) { + const uncachedInput = inputTokens - cachedInputTokens; + const costSoFar = + (uncachedInput / 1_000_000) * pricing.inputCostPerM + + (cachedInputTokens / 1_000_000) * pricing.cachedInputCostPerM + + (outputTokens / 1_000_000) * pricing.outputCostPerM; + const estTotalCost = + costSoFar + + ((remaining * (avgIn - avgCachedIn)) / 1_000_000) * pricing.inputCostPerM + + ((remaining * avgCachedIn) / 1_000_000) * pricing.cachedInputCostPerM + + ((remaining * avgOut) / 1_000_000) * pricing.outputCostPerM; + params.onProgress?.( + `[summarize] ${index}/${pending.length} thread #${row.number} | cost=$${costSoFar.toFixed(2)} est_total=$${estTotalCost.toFixed(2)} | avg_in=${Math.round(avgIn)} avg_out=${Math.round(avgOut)} | ETA ${etaStr}`, + ); + } else { + params.onProgress?.( + `[summarize] ${index}/${pending.length} thread #${row.number} | avg_in=${Math.round(avgIn)} avg_out=${Math.round(avgOut)} | ETA ${etaStr}`, + ); + } } summarized += 1; } @@ -1198,8 +1220,15 @@ export class GHCrawlService { const runId = this.startRun('embedding_runs', repository.id, params.threadNumber ? `thread:${params.threadNumber}` : repository.fullName); try { - if (!this.isRepoVectorStateCurrent(repository.id) && params.threadNumber === undefined) { - this.resetRepositoryVectors(repository.id, repository.fullName); + if (params.threadNumber === undefined) { + if (!this.isRepoVectorStateCurrent(repository.id)) { + this.resetRepositoryVectors(repository.id, repository.fullName); + } else { + const pruned = this.pruneInactiveRepositoryVectors(repository.id, repository.fullName); + if (pruned > 0) { + params.onProgress?.(`[embed] pruned ${pruned} closed or inactive vector(s) before refresh`); + } + } } const { rows, tasks, pending, missingSummaryThreadNumbers } = this.getEmbeddingWorkset(repository.id, params.threadNumber); @@ -1380,8 +1409,13 @@ export class GHCrawlService { const backend = params.backend ?? 'vectorlite'; const repository = this.requireRepository(params.owner, params.repo); const loaded = this.loadClusterableThreadMeta(repository.id); - const sourceKinds = params.sourceKinds ?? loaded.sourceKinds; - const items = loaded.items; + const activeVectors = this.isRepoVectorStateCurrent(repository.id) ? this.loadNormalizedActiveVectors(repository.id) : []; + const activeSourceKind: EmbeddingSourceKind = this.config.embeddingBasis === 'title_summary' ? 'dedupe_summary' : 'body'; + const useActiveVectors = activeVectors.length > 0 && (params.sourceKinds === undefined || loaded.items.length === 0); + const sourceKinds = useActiveVectors ? [activeSourceKind] : (params.sourceKinds ?? loaded.sourceKinds); + const items = useActiveVectors + ? activeVectors.map((item) => ({ id: item.id, number: item.number, title: item.title })) + : loaded.items; const aggregation = params.aggregation ?? 'max'; const minScore = params.minScore ?? DEFAULT_CLUSTER_MIN_SCORE; const k = params.k ?? 6; @@ -1398,6 +1432,12 @@ export class GHCrawlService { }; recordMemory(); + if (useActiveVectors && params.sourceKinds && loaded.items.length === 0) { + params.onProgress?.( + `[cluster-experiment] legacy source embeddings are unavailable for ${repository.fullName}; falling back to active ${this.config.embeddingBasis} vectors`, + ); + } + params.onProgress?.( `[cluster-experiment] loaded ${items.length} embedded thread(s) across ${sourceKinds.length} source kind(s) for ${repository.fullName} backend=${backend} k=${k} candidateK=${candidateK} minScore=${minScore} aggregation=${aggregation}`, ); @@ -1415,12 +1455,9 @@ export class GHCrawlService { try { if (backend === 'exact') { - const totalItems = sourceKinds.reduce((sum, sourceKind) => sum + this.countEmbeddingsForSourceKind(repository.id, sourceKind), 0); - let processedItems = 0; - - for (const sourceKind of sourceKinds) { + if (useActiveVectors) { const loadStartedAt = Date.now(); - const normalizedRows = this.loadNormalizedEmbeddingsForSourceKind(repository.id, sourceKind); + const normalizedRows = activeVectors.map(({ id, embedding }) => ({ id, normalizedEmbedding: embedding })); loadMs += Date.now() - loadStartedAt; recordMemory(); @@ -1433,14 +1470,41 @@ export class GHCrawlService { recordMemory(); if (!params.onProgress) return; params.onProgress( - `[cluster-experiment] exact ${processedItems + progress.processedItems}/${totalItems} source embeddings processed current_edges~=${perSourceScores.size + progress.currentEdgeEstimate}`, + `[cluster-experiment] exact ${progress.processedItems}/${normalizedRows.length} active vectors processed current_edges~=${perSourceScores.size + progress.currentEdgeEstimate}`, ); }, }); edgeBuildMs += Date.now() - edgesStartedAt; - processedItems += normalizedRows.length; - this.collectSourceKindScores(perSourceScores, edges, sourceKind); + this.collectSourceKindScores(perSourceScores, edges, activeSourceKind); recordMemory(); + } else { + const totalItems = sourceKinds.reduce((sum, sourceKind) => sum + this.countEmbeddingsForSourceKind(repository.id, sourceKind), 0); + let processedItems = 0; + + for (const sourceKind of sourceKinds) { + const loadStartedAt = Date.now(); + const normalizedRows = this.loadNormalizedEmbeddingsForSourceKind(repository.id, sourceKind); + loadMs += Date.now() - loadStartedAt; + recordMemory(); + + const edgesStartedAt = Date.now(); + const edges = buildSourceKindEdges(normalizedRows, { + limit: k, + minScore, + progressIntervalMs: CLUSTER_PROGRESS_INTERVAL_MS, + onProgress: (progress) => { + recordMemory(); + if (!params.onProgress) return; + params.onProgress( + `[cluster-experiment] exact ${processedItems + progress.processedItems}/${totalItems} source embeddings processed current_edges~=${perSourceScores.size + progress.currentEdgeEstimate}`, + ); + }, + }); + edgeBuildMs += Date.now() - edgesStartedAt; + processedItems += normalizedRows.length; + this.collectSourceKindScores(perSourceScores, edges, sourceKind); + recordMemory(); + } } } else { const setupStartedAt = Date.now(); @@ -1455,23 +1519,33 @@ export class GHCrawlService { setupMs += Date.now() - setupStartedAt; recordMemory(); - for (const sourceKind of sourceKinds) { - const sourceRowCount = this.countEmbeddingsForSourceKind(repository.id, sourceKind); + const vectorSources = useActiveVectors + ? [ + { + sourceKind: activeSourceKind, + rows: activeVectors.map(({ id, embedding }) => ({ id, normalizedEmbedding: embedding })), + }, + ] + : sourceKinds.map((sourceKind) => ({ + sourceKind, + rows: this.loadNormalizedEmbeddingsForSourceKind(repository.id, sourceKind).map((row) => ({ + id: row.id, + normalizedEmbedding: row.normalizedEmbedding, + })), + })); + + for (const source of vectorSources) { + const sourceRowCount = source.rows.length; if (sourceRowCount === 0) { continue; } - const firstRow = this.loadNormalizedEmbeddingForSourceKindHead(repository.id, sourceKind); - if (!firstRow) { - continue; - } - - const tableName = `vector_${sourceKind}`; - const dimension = firstRow.normalizedEmbedding.length; + const dimension = source.rows[0]!.normalizedEmbedding.length; const safeCandidateK = Math.min(candidateK, Math.max(1, sourceRowCount - 1)); + const tableName = `vector_${source.sourceKind}`; params.onProgress?.( - `[cluster-experiment] building ${sourceKind} HNSW index with ${sourceRowCount} vector(s)`, + `[cluster-experiment] building ${source.sourceKind} HNSW index with ${sourceRowCount} vector(s)`, ); const indexStartedAt = Date.now(); tempDb.exec( @@ -1480,7 +1554,7 @@ export class GHCrawlService { const insert = tempDb.prepare(`insert into ${tableName}(rowid, vec) values (?, ?)`); tempDb.transaction(() => { const loadStartedAt = Date.now(); - for (const row of this.iterateNormalizedEmbeddingsForSourceKind(repository.id, sourceKind)) { + for (const row of source.rows) { insert.run(row.id, this.normalizedEmbeddingBuffer(row.normalizedEmbedding)); } loadMs += Date.now() - loadStartedAt; @@ -1497,7 +1571,7 @@ export class GHCrawlService { let processed = 0; let lastProgressAt = Date.now(); const queryLoadStartedAt = Date.now(); - for (const row of this.iterateNormalizedEmbeddingsForSourceKind(repository.id, sourceKind)) { + for (const row of source.rows) { const candidates = query.all(this.normalizedEmbeddingBuffer(row.normalizedEmbedding)) as Array<{ rowid: number; distance: number; @@ -1518,11 +1592,11 @@ export class GHCrawlService { const key = this.edgeKey(row.id, candidate.item.rowid); const existing = perSourceScores.get(key); if (existing) { - existing.scores.set(sourceKind, Math.max(existing.scores.get(sourceKind) ?? -1, score)); + existing.scores.set(source.sourceKind, Math.max(existing.scores.get(source.sourceKind) ?? -1, score)); continue; } const scores = new Map(); - scores.set(sourceKind, score); + scores.set(source.sourceKind, score); perSourceScores.set(key, { leftThreadId: Math.min(row.id, candidate.item.rowid), rightThreadId: Math.max(row.id, candidate.item.rowid), @@ -1535,7 +1609,7 @@ export class GHCrawlService { if (params.onProgress && now - lastProgressAt >= CLUSTER_PROGRESS_INTERVAL_MS) { recordMemory(); params.onProgress( - `[cluster-experiment] querying ${sourceKind} index ${processed}/${sourceRowCount} current_edges=${perSourceScores.size} added_this_step=${addedThisRow}`, + `[cluster-experiment] querying ${source.sourceKind} index ${processed}/${sourceRowCount} current_edges=${perSourceScores.size} added_this_step=${addedThisRow}`, ); lastProgressAt = now; } @@ -2596,6 +2670,34 @@ export class GHCrawlService { }); } + private pruneInactiveRepositoryVectors(repoId: number, repoFullName: string): number { + const rows = this.db + .prepare( + `select tv.thread_id + from thread_vectors tv + join threads t on t.id = tv.thread_id + where t.repo_id = ? + and (t.state != 'open' or t.closed_at_local is not null)`, + ) + .all(repoId) as Array<{ thread_id: number }>; + if (rows.length === 0) { + return 0; + } + + const deleteVectorRow = this.db.prepare('delete from thread_vectors where thread_id = ?'); + this.db.transaction(() => { + for (const row of rows) { + deleteVectorRow.run(row.thread_id); + this.vectorStore.deleteVector({ + storePath: this.repoVectorStorePath(repoFullName), + dimensions: ACTIVE_EMBED_DIMENSIONS, + threadId: row.thread_id, + }); + } + })(); + return rows.length; + } + private cleanupMigratedRepositoryArtifacts(repoId: number, repoFullName: string, onProgress?: (message: string) => void): void { const legacyEmbeddingCount = this.countLegacyEmbeddings(repoId); const inlineJsonVectorCount = this.countInlineJsonThreadVectors(repoId); @@ -3816,6 +3918,15 @@ export class GHCrawlService { })); } + private loadNormalizedActiveVectors(repoId: number): Array<{ id: number; number: number; title: string; embedding: number[] }> { + return this.loadClusterableActiveVectorMeta(repoId, '').map((row) => ({ + id: row.id, + number: row.number, + title: row.title, + embedding: normalizeEmbedding(row.embedding).normalized, + })); + } + private listStoredClusterNeighbors(repoId: number, threadId: number, limit: number): SearchHitDto['neighbors'] { const latestRun = this.getLatestClusterRun(repoId); if (!latestRun) { From 27672e794a15b9c4c227ce29911b82080ac8e382 Mon Sep 17 00:00:00 2001 From: huntharo Date: Fri, 3 Apr 2026 15:22:27 -0400 Subject: [PATCH 33/34] ci: remove experimental cluster perf workflow --- .github/workflows/ci.yml | 179 +-------------------------------------- 1 file changed, 3 insertions(+), 176 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18eb5d2..cf87a71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,194 +66,21 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v6 - with: - fetch-depth: 0 - name: Install Node.js and dependencies uses: ./.github/actions/configure-nodejs - - name: Create baseline worktree for pull requests - if: ${{ github.event_name == 'pull_request' }} - run: git worktree add --detach "$RUNNER_TEMP/cluster-perf-base" "${{ github.event.pull_request.base.sha }}" - - - name: Copy large cluster perf fixture into baseline worktree - if: ${{ github.event_name == 'pull_request' }} - run: | - cp packages/api-core/src/cluster/perf-large.json "$RUNNER_TEMP/cluster-perf-base/packages/api-core/src/cluster/perf-baseline.json" - cp packages/api-core/src/cluster/perf.integration.ts "$RUNNER_TEMP/cluster-perf-base/packages/api-core/src/cluster/perf.integration.ts" - - - name: Install dependencies for baseline worktree - if: ${{ github.event_name == 'pull_request' }} - working-directory: ${{ runner.temp }}/cluster-perf-base - run: pnpm install --frozen-lockfile - - - name: Run baseline exact cluster perf - if: ${{ github.event_name == 'pull_request' }} - env: - GHCRAWL_CLUSTER_PERF_IGNORE_THRESHOLD: 1 - GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf-base-exact.json - working-directory: ${{ runner.temp }}/cluster-perf-base - run: pnpm test:cluster-perf - - - name: Run PR head exact cluster perf + - name: Run cluster performance integration test env: - GHCRAWL_CLUSTER_PERF_CONFIG_PATH: ${{ github.workspace }}/packages/api-core/src/cluster/perf-large.json - GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf-head-exact.json + GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf.json run: pnpm test:cluster-perf - - name: Run PR head vectorlite cluster perf - if: ${{ github.event_name == 'pull_request' }} - env: - GHCRAWL_CLUSTER_PERF_BACKEND: vectorlite - GHCRAWL_CLUSTER_PERF_CONFIG_PATH: ${{ github.workspace }}/packages/api-core/src/cluster/perf-large.json - GHCRAWL_CLUSTER_PERF_IGNORE_THRESHOLD: 1 - GHCRAWL_CLUSTER_PERF_OUTPUT_PATH: ${{ runner.temp }}/cluster-perf-head-vectorlite.json - run: | - for attempt in 1 2 3; do - echo "Vectorlite perf attempt ${attempt}/3" - pnpm test:cluster-perf - done - - - name: Build combined cluster perf report - env: - GH_EVENT_NAME: ${{ github.event_name }} - BASE_REPORT_PATH: ${{ runner.temp }}/cluster-perf-base-exact.json - HEAD_EXACT_REPORT_PATH: ${{ runner.temp }}/cluster-perf-head-exact.json - HEAD_VECTORLITE_REPORT_PATH: ${{ runner.temp }}/cluster-perf-head-vectorlite.json - COMBINED_REPORT_PATH: ${{ runner.temp }}/cluster-perf.json - run: | - node <<'EOF' - const fs = require('node:fs'); - - function readReport(path) { - if (!path || !fs.existsSync(path)) { - return null; - } - return JSON.parse(fs.readFileSync(path, 'utf8')); - } - - function formatDurationMs(durationMs) { - if (!Number.isFinite(durationMs)) return 'n/a'; - if (durationMs < 1000) return `${durationMs.toFixed(1)} ms`; - const totalSeconds = durationMs / 1000; - if (totalSeconds < 60) return `${totalSeconds.toFixed(2)} s`; - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds - minutes * 60; - return `${minutes}m ${seconds.toFixed(1)}s`; - } - - function formatBytes(bytes) { - if (!Number.isFinite(bytes)) return 'n/a'; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`; - return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`; - } - - function formatPercent(value) { - const sign = value > 0 ? '+' : ''; - return `${sign}${value.toFixed(1)}%`; - } - - const eventName = process.env.GH_EVENT_NAME; - const base = readReport(process.env.BASE_REPORT_PATH); - const headExact = readReport(process.env.HEAD_EXACT_REPORT_PATH); - const headVectorlite = readReport(process.env.HEAD_VECTORLITE_REPORT_PATH); - - if (!headExact) { - throw new Error('Missing PR/head exact cluster perf report'); - } - - const lines = [ - '## Cluster Performance', - '', - `- Fixture shape: ${headExact.result.threadCount} threads x ${headExact.result.sourceKinds.length} source kinds`, - `- Head exact fixture median (cluster-only): ${formatDurationMs(headExact.result.medianMs)}`, - `- Head exact fixture median (total run): ${formatDurationMs(headExact.result.totalMedianMs)}`, - `- Head exact edge-build median: ${formatDurationMs(headExact.result.edgeBuildMedianMs)}`, - `- Head exact cluster-assembly median: ${formatDurationMs(headExact.result.clusterBuildMedianMs)}`, - `- Head exact median peak RSS: ${formatBytes(headExact.result.medianPeakRssBytes)}`, - `- Head exact median peak heap used: ${formatBytes(headExact.result.medianPeakHeapUsedBytes)}`, - `- Head exact projected openclaw/openclaw: ${formatDurationMs(headExact.result.projectedOpenclawMs)}`, - ]; - - if (eventName === 'pull_request' && base) { - const exactDeltaMs = headExact.result.medianMs - base.result.medianMs; - const exactDeltaPercent = base.result.medianMs > 0 ? (exactDeltaMs / base.result.medianMs) * 100 : 0; - const exactProjectedDeltaMs = headExact.result.projectedOpenclawMs - base.result.projectedOpenclawMs; - const exactProjectedDeltaPercent = - base.result.projectedOpenclawMs > 0 ? (exactProjectedDeltaMs / base.result.projectedOpenclawMs) * 100 : 0; - lines.push( - `- Base exact fixture median (cluster-only): ${formatDurationMs(base.result.medianMs)}`, - `- Exact delta vs base: ${formatDurationMs(exactDeltaMs)} (${formatPercent(exactDeltaPercent)})`, - `- Base exact projected openclaw/openclaw: ${formatDurationMs(base.result.projectedOpenclawMs)}`, - `- Exact projected delta vs base: ${formatDurationMs(exactProjectedDeltaMs)} (${formatPercent(exactProjectedDeltaPercent)})`, - ); - } - - if (headVectorlite) { - const vectorliteDeltaMs = headVectorlite.result.medianMs - headExact.result.medianMs; - const vectorliteDeltaPercent = - headExact.result.medianMs > 0 ? (vectorliteDeltaMs / headExact.result.medianMs) * 100 : 0; - const vectorliteProjectedDeltaMs = headVectorlite.result.projectedOpenclawMs - headExact.result.projectedOpenclawMs; - const vectorliteProjectedDeltaPercent = - headExact.result.projectedOpenclawMs > 0 ? (vectorliteProjectedDeltaMs / headExact.result.projectedOpenclawMs) * 100 : 0; - lines.push( - '', - `- Head vectorlite fixture median (cluster-only, run 3/3): ${formatDurationMs(headVectorlite.result.medianMs)}`, - `- Head vectorlite fixture median (total run, run 3/3): ${formatDurationMs(headVectorlite.result.totalMedianMs)}`, - `- Head vectorlite setup median: ${formatDurationMs(headVectorlite.result.setupMedianMs)}`, - `- Head vectorlite index-build median: ${formatDurationMs(headVectorlite.result.indexBuildMedianMs)}`, - `- Head vectorlite query median: ${formatDurationMs(headVectorlite.result.queryMedianMs)}`, - `- Head vectorlite cluster-assembly median: ${formatDurationMs(headVectorlite.result.clusterBuildMedianMs)}`, - `- Head vectorlite median peak RSS: ${formatBytes(headVectorlite.result.medianPeakRssBytes)}`, - `- Head vectorlite median peak heap used: ${formatBytes(headVectorlite.result.medianPeakHeapUsedBytes)}`, - `- Vectorlite delta vs head exact: ${formatDurationMs(vectorliteDeltaMs)} (${formatPercent(vectorliteDeltaPercent)})`, - `- Head vectorlite projected openclaw/openclaw: ${formatDurationMs(headVectorlite.result.projectedOpenclawMs)}`, - `- Vectorlite projected delta vs head exact: ${formatDurationMs(vectorliteProjectedDeltaMs)} (${formatPercent(vectorliteProjectedDeltaPercent)})`, - ); - } - - lines.push('', '### Raw Runs', '', '#### Head Exact', '', headExact.summary); - if (eventName === 'pull_request' && base) { - lines.push('', '#### Base Exact', '', base.summary); - } - if (headVectorlite) { - lines.push('', '#### Head Vectorlite', '', headVectorlite.summary); - } - - const summary = `${lines.join('\n')}\n`; - fs.writeFileSync( - process.env.COMBINED_REPORT_PATH, - JSON.stringify( - { - status: 'PASS', - summary, - reports: { - baseExact: base, - headExact, - headVectorlite, - }, - }, - null, - 2, - ) + '\n', - ); - - const summaryPath = process.env.GITHUB_STEP_SUMMARY; - if (summaryPath) { - fs.appendFileSync(summaryPath, summary); - } - EOF - - name: Upload cluster performance report artifact if: ${{ always() }} uses: actions/upload-artifact@v7 with: name: cluster-perf-report - path: | - ${{ runner.temp }}/cluster-perf.json - ${{ runner.temp }}/cluster-perf-base-exact.json - ${{ runner.temp }}/cluster-perf-head-exact.json - ${{ runner.temp }}/cluster-perf-head-vectorlite.json + path: ${{ runner.temp }}/cluster-perf.json if-no-files-found: ignore cli-smoke: From 3ffa673695515e4fc55551ba230e219a2f53ade0 Mon Sep 17 00:00:00 2001 From: huntharo Date: Fri, 3 Apr 2026 15:28:55 -0400 Subject: [PATCH 34/34] docs: add one-time vector migration notes --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 498bd86..b409617 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,27 @@ ghcrawl tui owner/repo `ghcrawl refresh owner/repo` is the main pipeline command. It pulls the latest open GitHub issues and pull requests, summarizes changed items only when the active embedding basis depends on summaries, refreshes vectors, and rebuilds the clusters you browse in the TUI. +## One-Time Migration + +Upgrading to this release changes the local vector and cluster pipeline: + +- vectors now use a persistent `vectorlite` sidecar index +- the active vector is one vector per open thread +- old multi-row `document_embeddings` are removed after the first successful rebuild + +For an existing repo, the one-time migration command is: + +```bash +ghcrawl refresh owner/repo +``` + +Important notes: + +- `refresh` performs the migration; plain `sync` does not +- with the default `title_original` basis, the migration rebuilds vectors and clusters without running LLM summaries +- if you switch to `title_summary`, `refresh` also runs the summarize step before embedding +- after the first successful migration refresh, ghcrawl removes legacy embeddings, compacts the local DB, and rebuilds clusters from the current vectors + ## Typical Commands ```bash