@todoist/doistbot-cli 0.0.0-stage → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/auth.js +225 -0
- package/dist/actions/doctor.js +39 -0
- package/dist/actions/review-usage.js +25 -0
- package/dist/actions/review.js +311 -0
- package/dist/actions/setup.js +14 -0
- package/dist/auth.js +202 -0
- package/dist/config.js +145 -0
- package/dist/env.js +10 -0
- package/dist/file-store.js +112 -0
- package/dist/index.js +164 -0
- package/dist/input.js +40 -0
- package/dist/runtime.js +105 -0
- package/dist/secure-store.js +148 -0
- package/dist/terminal.js +185 -0
- package/dist/test-utils/secure-store-mocks.js +29 -0
- package/dist/update-cache-entrypoint.js +16 -0
- package/dist/update-check.js +235 -0
- package/package.json +65 -5
- package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/_node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/_node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/_node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/dist/core/autofix-resources.js +58 -0
- package/sandbox/dist/core/autofix-service-query.js +69 -0
- package/sandbox/dist/core/autofix-test-policy.js +11 -0
- package/sandbox/dist/core/autofix-verification.js +355 -0
- package/sandbox/dist/core/bot-logins.js +14 -0
- package/sandbox/dist/core/check-run.js +158 -0
- package/sandbox/dist/core/datadog-metrics.js +113 -0
- package/sandbox/dist/core/environment.js +49 -0
- package/sandbox/dist/core/git-auth.js +17 -0
- package/sandbox/dist/core/github-history.js +4 -0
- package/sandbox/dist/core/github-issues.js +18 -0
- package/sandbox/dist/core/label-parsing.js +1 -0
- package/sandbox/dist/core/langfuse-observation.js +33 -0
- package/sandbox/dist/core/langfuse-session.js +39 -0
- package/sandbox/dist/core/logger-span-processor.js +90 -0
- package/sandbox/dist/core/logging.js +18 -0
- package/sandbox/dist/core/pi-autofix-extension.js +188 -0
- package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
- package/sandbox/dist/core/pi-progress-logging.js +51 -0
- package/sandbox/dist/core/pi-tool-output-filter-extension.js +65 -0
- package/sandbox/dist/core/pi.js +870 -0
- package/sandbox/dist/core/pii-scrubber.js +31 -0
- package/sandbox/dist/core/prompt-template.js +63 -0
- package/sandbox/dist/core/reactions.js +36 -0
- package/sandbox/dist/core/regexp.js +3 -0
- package/sandbox/dist/core/repository-map.js +1 -0
- package/sandbox/dist/core/repository-resolver.js +1 -0
- package/sandbox/dist/core/retry.js +29 -0
- package/sandbox/dist/core/s3-utils.js +42 -0
- package/sandbox/dist/core/shared.js +124 -0
- package/sandbox/dist/core/snapshot-helpers.js +17 -0
- package/sandbox/dist/core/span-test-helpers.js +13 -0
- package/sandbox/dist/core/tool-environment.js +83 -0
- package/sandbox/dist/core/tool-output.js +18 -0
- package/sandbox/dist/core/tracing-setup.js +92 -0
- package/sandbox/dist/core/tracing.js +81 -0
- package/sandbox/dist/main.js +51 -0
- package/sandbox/dist/providers/config.js +23 -0
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +28 -0
- package/sandbox/dist/providers/helpers.js +253 -0
- package/sandbox/dist/providers/index.js +27 -0
- package/sandbox/dist/providers/openai.js +23 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +301 -0
- package/sandbox/dist/providers/process.js +34 -0
- package/sandbox/dist/providers/schemas.js +61 -0
- package/sandbox/dist/providers/types.js +4 -0
- package/sandbox/dist/tasks/chat/chat.js +440 -0
- package/sandbox/dist/tasks/chat/prompt.js +20 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +304 -0
- package/sandbox/dist/tasks/issue-summarize/context.js +21 -0
- package/sandbox/dist/tasks/issue-summarize/media.js +335 -0
- package/sandbox/dist/tasks/issue-summarize/model.js +230 -0
- package/sandbox/dist/tasks/issue-summarize/output.js +45 -0
- package/sandbox/dist/tasks/issue-summarize/prompt.js +11 -0
- package/sandbox/dist/tasks/issue-summarize/summarize.js +238 -0
- package/sandbox/dist/tasks/issue-summarize/thread.js +123 -0
- package/sandbox/dist/tasks/issue-summarize/types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
- package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
- package/sandbox/dist/tasks/issue-triage/autofix-checkpoint.js +122 -0
- package/sandbox/dist/tasks/issue-triage/autofix-policy.js +36 -0
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +57 -0
- package/sandbox/dist/tasks/issue-triage/diff-validator.js +102 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +617 -0
- package/sandbox/dist/tasks/issue-triage/fix-context.js +5 -0
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +263 -0
- package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +526 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +108 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/recovery.js +512 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +233 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +171 -0
- package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +206 -0
- package/sandbox/dist/tasks/issue-triage/markers.js +6 -0
- package/sandbox/dist/tasks/issue-triage/model.js +124 -0
- package/sandbox/dist/tasks/issue-triage/output.js +164 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +585 -0
- package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
- package/sandbox/dist/tasks/issue-triage/prompt.js +63 -0
- package/sandbox/dist/tasks/issue-triage/repo-config.js +47 -0
- package/sandbox/dist/tasks/issue-triage/repo-prompts.js +77 -0
- package/sandbox/dist/tasks/issue-triage/report-fixture.js +53 -0
- package/sandbox/dist/tasks/issue-triage/report-history.js +113 -0
- package/sandbox/dist/tasks/issue-triage/report-schema.js +63 -0
- package/sandbox/dist/tasks/issue-triage/report.js +111 -0
- package/sandbox/dist/tasks/issue-triage/resolution.js +30 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +169 -0
- package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +59 -0
- package/sandbox/dist/tasks/issue-triage/shared.js +86 -0
- package/sandbox/dist/tasks/issue-triage/side-effects.js +102 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +442 -0
- package/sandbox/dist/tasks/issue-triage/types.js +1 -0
- package/sandbox/dist/tasks/persist-logs/context.js +19 -0
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +116 -0
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +156 -0
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +491 -0
- package/sandbox/dist/tasks/refresh-s3-urls/refresh-s3-urls.js +214 -0
- package/sandbox/dist/tasks/review/check-run.js +326 -0
- package/sandbox/dist/tasks/review/comment-mapper.js +46 -0
- package/sandbox/dist/tasks/review/conversation-context.js +477 -0
- package/sandbox/dist/tasks/review/diff.js +95 -0
- package/sandbox/dist/tasks/review/engines/dedupe.js +291 -0
- package/sandbox/dist/tasks/review/engines/multi-focus.js +419 -0
- package/sandbox/dist/tasks/review/engines/shared.js +87 -0
- package/sandbox/dist/tasks/review/engines/types.js +1 -0
- package/sandbox/dist/tasks/review/finding-output.js +18 -0
- package/sandbox/dist/tasks/review/format.js +17 -0
- package/sandbox/dist/tasks/review/github-comment-format.js +94 -0
- package/sandbox/dist/tasks/review/local-git.js +374 -0
- package/sandbox/dist/tasks/review/local.js +118 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +59 -0
- package/sandbox/dist/tasks/review/prompt.js +81 -0
- package/sandbox/dist/tasks/review/review-feedback.js +48 -0
- package/sandbox/dist/tasks/review/review-mode-github.js +58 -0
- package/sandbox/dist/tasks/review/review-mode-policy.js +232 -0
- package/sandbox/dist/tasks/review/review-mode-types.js +1 -0
- package/sandbox/dist/tasks/review/review-mode.js +296 -0
- package/sandbox/dist/tasks/review/review-summary.js +123 -0
- package/sandbox/dist/tasks/review/review.js +391 -0
- package/sandbox/dist/tasks/review/runner.js +255 -0
- package/sandbox/dist/tasks/review/severity.js +5 -0
- package/sandbox/dist/tasks/review/summary-model.js +271 -0
- package/sandbox/dist/tasks/review/thinking-level.js +14 -0
- package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
- package/sandbox/dist/tasks/review/types.js +1 -0
- package/sandbox/dist/tasks/review-slice-plan/context.js +66 -0
- package/sandbox/dist/tasks/review-slice-plan/plan.js +210 -0
- package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
- package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +367 -0
- package/sandbox/dist/tasks/sentry-triage/store.js +133 -0
- package/sandbox/dist/tasks/sentry-triage/triage.js +217 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/src/review/prompts/ai-internal-tools.md +254 -0
- package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
- package/sandbox/src/review/prompts/observability-standard.md +169 -0
- package/sandbox/src/review/prompts/outline-map.json +26 -0
- package/sandbox/src/review/prompts/secrets-management.md +286 -0
- package/sandbox/src/review/prompts/service-production-readiness.md +122 -0
- package/sandbox/src/review/prompts/workload-placement.md +345 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +16 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +32 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +20 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +21 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +19 -0
- package/sandbox/src/tasks/review/prompts/review-mode.md +40 -0
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +142 -0
- package/scripts/restore-runtime-assets.mjs +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { performance } from 'node:perf_hooks';
|
|
2
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
3
|
+
import { repositoryKey } from 'doistbot-issue-routing';
|
|
4
|
+
import { isKnownDoistbotLogin } from '../../core/bot-logins.js';
|
|
5
|
+
import { recordTaskMetrics } from '../../core/datadog-metrics.js';
|
|
6
|
+
import { githubHistoryIdentity } from '../../core/github-history.js';
|
|
7
|
+
import { logger } from '../../core/logging.js';
|
|
8
|
+
import { removeIssueCommentReactionIfPresent } from '../../core/reactions.js';
|
|
9
|
+
import { resolveIssueRepositories } from '../../core/repository-resolver.js';
|
|
10
|
+
import { recordDecision, withPhase } from '../../core/tracing.js';
|
|
11
|
+
import { shutdownTracing } from '../../core/tracing-setup.js';
|
|
12
|
+
import { parseIssueTriageContext } from '../issue-triage/context.js';
|
|
13
|
+
import { postAutoFixFailureComment } from '../issue-triage/fix-attempt.js';
|
|
14
|
+
import { runConfiguredFix } from '../issue-triage/fix-dispatch.js';
|
|
15
|
+
import { parseFixAssessment } from '../issue-triage/output.js';
|
|
16
|
+
import { readTriageReport, reportAssessment, reportForFix } from '../issue-triage/report.js';
|
|
17
|
+
import { cloneRepository, createGitHubClients, extractLabelNames, fetchIssue, fetchIssueComments, getIssueUrl, prepareWorkspaceReposDirectory, TRIAGE_MARKER, } from '../issue-triage/shared.js';
|
|
18
|
+
export async function main() {
|
|
19
|
+
const startedAt = performance.now();
|
|
20
|
+
const ctx = parseIssueTriageContext();
|
|
21
|
+
let exitCode = 0;
|
|
22
|
+
let metricOutcome = 'failure';
|
|
23
|
+
let metricRepository = `${ctx.owner}/${ctx.repo}`;
|
|
24
|
+
logger.setBaseContext({
|
|
25
|
+
deliveryId: ctx.deliveryId,
|
|
26
|
+
repository: `${ctx.owner}/${ctx.repo}`,
|
|
27
|
+
issueNumber: ctx.issueNumber,
|
|
28
|
+
});
|
|
29
|
+
// Keep manual command metrics under the retry task while the shared loop runs.
|
|
30
|
+
await withPhase('issue-fix-retry', {
|
|
31
|
+
task: 'issue-fix-retry',
|
|
32
|
+
'gen_ai.operation.name': 'invoke_agent',
|
|
33
|
+
'gen_ai.agent.name': 'doistbot.issue-fix-retry',
|
|
34
|
+
'issue.number': ctx.issueNumber,
|
|
35
|
+
'comment.id': ctx.commentId,
|
|
36
|
+
dry_run: ctx.dryRun,
|
|
37
|
+
}, async (span) => {
|
|
38
|
+
const { app, readOctokit, writeOctokit } = await createGitHubClients(ctx);
|
|
39
|
+
async function postComment(body) {
|
|
40
|
+
if (ctx.dryRun)
|
|
41
|
+
return;
|
|
42
|
+
await writeOctokit.rest.issues.createComment({
|
|
43
|
+
owner: ctx.owner,
|
|
44
|
+
repo: ctx.repo,
|
|
45
|
+
issue_number: ctx.issueNumber,
|
|
46
|
+
body,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const [issue, comments] = await Promise.all([
|
|
51
|
+
fetchIssue(readOctokit, ctx),
|
|
52
|
+
fetchIssueComments(readOctokit, ctx),
|
|
53
|
+
]);
|
|
54
|
+
// Find the most recent triage comment from the bot (search backwards to get the latest one)
|
|
55
|
+
let triageComment;
|
|
56
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
57
|
+
const comment = comments[i];
|
|
58
|
+
if (comment &&
|
|
59
|
+
isKnownDoistbotLogin(comment.user?.login) &&
|
|
60
|
+
comment.body?.includes(TRIAGE_MARKER)) {
|
|
61
|
+
triageComment = comment;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (!triageComment?.body) {
|
|
66
|
+
// User-actionable (no prior triage), not a malfunction → span stays ok.
|
|
67
|
+
logger.warn('No triage comment found for fix retry', {
|
|
68
|
+
task: 'issue-fix-retry',
|
|
69
|
+
issueNumber: ctx.issueNumber,
|
|
70
|
+
});
|
|
71
|
+
await postComment('No previous triage found on this issue. Run `@doistbot /triage` first, then retry with `@doistbot /fix`.');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Strip the triage marker to get the raw triage output
|
|
75
|
+
const commentOutput = triageComment.body.replace(TRIAGE_MARKER, '').trim();
|
|
76
|
+
let report;
|
|
77
|
+
let fixAssessment;
|
|
78
|
+
try {
|
|
79
|
+
report = readTriageReport(commentOutput);
|
|
80
|
+
fixAssessment = report
|
|
81
|
+
? reportAssessment(report)
|
|
82
|
+
: parseFixAssessment(commentOutput);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
report = null;
|
|
86
|
+
fixAssessment = null;
|
|
87
|
+
}
|
|
88
|
+
if (!fixAssessment) {
|
|
89
|
+
logger.warn('No fix assessment found in triage output', {
|
|
90
|
+
task: 'issue-fix-retry',
|
|
91
|
+
issueNumber: ctx.issueNumber,
|
|
92
|
+
});
|
|
93
|
+
await postComment('The previous triage has no valid fix assessment. Run `@doistbot /triage` again before requesting a fix.');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (report &&
|
|
97
|
+
githubHistoryIdentity(report.issueUrl) !==
|
|
98
|
+
githubHistoryIdentity(getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber))) {
|
|
99
|
+
await postComment('The stored triage report belongs to another issue. Run `@doistbot /triage` again.');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (report &&
|
|
103
|
+
fixAssessment.classification !== 'auto-fixable' &&
|
|
104
|
+
fixAssessment.classification !== 'logs-needed') {
|
|
105
|
+
await postComment('The latest triage needs more evidence or human input. Address its next steps and run `@doistbot /triage` again.');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const triageOutput = report ? reportForFix(report) : commentOutput;
|
|
109
|
+
if (fixAssessment.targetRepo) {
|
|
110
|
+
metricRepository = fixAssessment.targetRepo;
|
|
111
|
+
}
|
|
112
|
+
logger.info('Fix assessment found from previous triage', {
|
|
113
|
+
task: 'issue-fix-retry',
|
|
114
|
+
issueNumber: ctx.issueNumber,
|
|
115
|
+
classification: fixAssessment.classification,
|
|
116
|
+
confidence: fixAssessment.confidence,
|
|
117
|
+
scope: fixAssessment.scope,
|
|
118
|
+
});
|
|
119
|
+
// Resolve repositories to clone. Prefer the target repo from the assessment
|
|
120
|
+
// (stable across label changes) and fall back to label-based resolution.
|
|
121
|
+
const reposDir = prepareWorkspaceReposDirectory();
|
|
122
|
+
const clonedRepositories = [];
|
|
123
|
+
if (fixAssessment.targetRepo) {
|
|
124
|
+
const resolution = resolveIssueRepositories(extractLabelNames(issue.labels));
|
|
125
|
+
const repoTarget = resolution.status === 'resolved'
|
|
126
|
+
? resolution.repositories.find((r) => repositoryKey(r.fullName) ===
|
|
127
|
+
repositoryKey(fixAssessment.targetRepo))
|
|
128
|
+
: undefined;
|
|
129
|
+
if (repoTarget) {
|
|
130
|
+
try {
|
|
131
|
+
const cloneResult = await cloneRepository({
|
|
132
|
+
app,
|
|
133
|
+
repository: repoTarget,
|
|
134
|
+
reposDir,
|
|
135
|
+
fallbackInstallationId: ctx.installationId,
|
|
136
|
+
});
|
|
137
|
+
clonedRepositories.push(cloneResult);
|
|
138
|
+
metricRepository = cloneResult.repository.fullName;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
logger.warn('Fix retry clone failed for assessment target repo', {
|
|
142
|
+
task: 'issue-fix-retry',
|
|
143
|
+
repository: fixAssessment.targetRepo,
|
|
144
|
+
error: error instanceof Error ? error.message : String(error),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Fall back to label-based resolution if the assessment target wasn't cloned
|
|
150
|
+
if (clonedRepositories.length === 0) {
|
|
151
|
+
const labelNames = extractLabelNames(issue.labels);
|
|
152
|
+
const resolution = resolveIssueRepositories(labelNames);
|
|
153
|
+
if (resolution.status === 'unresolved') {
|
|
154
|
+
// User-actionable (labels don't map to a repo) → span stays ok.
|
|
155
|
+
logger.warn('Fix retry failed: could not resolve repositories from labels', {
|
|
156
|
+
task: 'issue-fix-retry',
|
|
157
|
+
issueNumber: ctx.issueNumber,
|
|
158
|
+
reason: resolution.reason,
|
|
159
|
+
});
|
|
160
|
+
await postComment(`Could not resolve target repositories from the issue labels. Please check the labels and try again.\n\nReason: ${resolution.reason}`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (resolution.repositories.length === 1) {
|
|
164
|
+
metricRepository = resolution.repositories[0].fullName;
|
|
165
|
+
}
|
|
166
|
+
for (const repository of resolution.repositories) {
|
|
167
|
+
try {
|
|
168
|
+
const cloneResult = await cloneRepository({
|
|
169
|
+
app,
|
|
170
|
+
repository,
|
|
171
|
+
reposDir,
|
|
172
|
+
fallbackInstallationId: ctx.installationId,
|
|
173
|
+
});
|
|
174
|
+
clonedRepositories.push(cloneResult);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
logger.warn('Fix retry clone failed', {
|
|
178
|
+
task: 'issue-fix-retry',
|
|
179
|
+
repository: repository.fullName,
|
|
180
|
+
error: error instanceof Error ? error.message : String(error),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (clonedRepositories.length === 0) {
|
|
186
|
+
// Infrastructure failure → mark the run failed.
|
|
187
|
+
const message = 'Fix retry failed: all repository clones failed';
|
|
188
|
+
logger.error(message, {
|
|
189
|
+
task: 'issue-fix-retry',
|
|
190
|
+
issueNumber: ctx.issueNumber,
|
|
191
|
+
});
|
|
192
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
193
|
+
await postComment('Fix retry failed — could not clone the target repositories. Please try again later.');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const issueInstallationAuth = (await app.octokit.auth({
|
|
197
|
+
type: 'installation',
|
|
198
|
+
installationId: ctx.installationId,
|
|
199
|
+
}));
|
|
200
|
+
const fixResult = await runConfiguredFix({
|
|
201
|
+
ctx,
|
|
202
|
+
octokit: writeOctokit,
|
|
203
|
+
triageOutput,
|
|
204
|
+
fixAssessment,
|
|
205
|
+
clonedRepos: clonedRepositories,
|
|
206
|
+
issueTitle: issue.title ?? 'Untitled issue',
|
|
207
|
+
issueUrl: issue.html_url ?? getIssueUrl(ctx.owner, ctx.repo, ctx.issueNumber),
|
|
208
|
+
ghToken: issueInstallationAuth.token,
|
|
209
|
+
retry: true,
|
|
210
|
+
phaseName: 'issue-fix-retry.fix-attempt',
|
|
211
|
+
assigneeReview: {
|
|
212
|
+
assignees: (issue.assignees ?? []).map((assignee) => assignee.login),
|
|
213
|
+
membershipOctokit: readOctokit,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
metricRepository = fixResult.repository;
|
|
217
|
+
metricOutcome = fixResult.attempted && fixResult.prCreated ? 'success' : 'failure';
|
|
218
|
+
const reason = 'reason' in fixResult ? fixResult.reason : undefined;
|
|
219
|
+
const isDryRun = 'dryRun' in fixResult && fixResult.dryRun === true;
|
|
220
|
+
recordDecision('issue-fix-retry.outcome', {
|
|
221
|
+
attempted: fixResult.attempted,
|
|
222
|
+
pr_created: fixResult.attempted && fixResult.prCreated,
|
|
223
|
+
reason,
|
|
224
|
+
pr_url: 'prUrl' in fixResult ? fixResult.prUrl : undefined,
|
|
225
|
+
});
|
|
226
|
+
// No PR is a failed retry — unless it was a dry run (expected).
|
|
227
|
+
if (!fixResult.attempted || !fixResult.prCreated) {
|
|
228
|
+
if (!isDryRun) {
|
|
229
|
+
span.setStatus({
|
|
230
|
+
code: SpanStatusCode.ERROR,
|
|
231
|
+
message: reason ?? 'Fix retry produced no PR',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (!ctx.dryRun) {
|
|
235
|
+
await postAutoFixFailureComment({
|
|
236
|
+
octokit: writeOctokit,
|
|
237
|
+
ctx,
|
|
238
|
+
reason: reason ?? 'Unknown reason',
|
|
239
|
+
targetRepoFullName: fixResult.repository,
|
|
240
|
+
...('prNumber' in fixResult ? { prNumber: fixResult.prNumber } : {}),
|
|
241
|
+
...('interruption' in fixResult
|
|
242
|
+
? { interruption: fixResult.interruption }
|
|
243
|
+
: {}),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
250
|
+
logger.error('Fix retry task failed', {
|
|
251
|
+
task: 'issue-fix-retry',
|
|
252
|
+
issueNumber: ctx.issueNumber,
|
|
253
|
+
error: message,
|
|
254
|
+
durationMs: performance.now() - startedAt,
|
|
255
|
+
});
|
|
256
|
+
// The catch swallows the error (posts a user message), so set the
|
|
257
|
+
// span status explicitly — withPhase only auto-marks ERROR on rethrow.
|
|
258
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
259
|
+
try {
|
|
260
|
+
await postComment('Sorry, the fix retry encountered an error. Please try again or contact the team maintaining the bot.');
|
|
261
|
+
}
|
|
262
|
+
catch (commentError) {
|
|
263
|
+
logger.warn('Failed to post fix retry failure response', {
|
|
264
|
+
task: 'issue-fix-retry',
|
|
265
|
+
issueNumber: ctx.issueNumber,
|
|
266
|
+
error: commentError instanceof Error
|
|
267
|
+
? commentError.message
|
|
268
|
+
: String(commentError),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
exitCode = 1;
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
await removeIssueCommentReactionIfPresent({
|
|
275
|
+
octokit: writeOctokit,
|
|
276
|
+
owner: ctx.owner,
|
|
277
|
+
repo: ctx.repo,
|
|
278
|
+
commentId: ctx.commentId,
|
|
279
|
+
reactionId: ctx.reactionId,
|
|
280
|
+
warnMessage: 'Failed to remove fix retry reaction',
|
|
281
|
+
successMessage: 'Fix retry reaction removed',
|
|
282
|
+
logMeta: {
|
|
283
|
+
task: 'issue-fix-retry',
|
|
284
|
+
reactionId: ctx.reactionId,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
await recordTaskMetrics({
|
|
288
|
+
type: 'fix',
|
|
289
|
+
repository: metricRepository,
|
|
290
|
+
model: 'openai',
|
|
291
|
+
outcome: metricOutcome,
|
|
292
|
+
durationMs: performance.now() - startedAt,
|
|
293
|
+
sandboxVariant: ctx.sandboxVariant,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
if (exitCode !== 0) {
|
|
298
|
+
await shutdownTracing();
|
|
299
|
+
process.exit(exitCode);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (process.env.VITEST !== 'true') {
|
|
303
|
+
main();
|
|
304
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { optionalPositiveInt, requiredEnv, requiredPositiveInt } from '../../core/environment.js';
|
|
2
|
+
export function parseSummarizeContext(env = process.env) {
|
|
3
|
+
const commentId = optionalPositiveInt(env.COMMENT_ID);
|
|
4
|
+
const reactionId = optionalPositiveInt(env.REACTION_ID);
|
|
5
|
+
const dryRun = env.DRY_RUN?.toLowerCase() === 'true';
|
|
6
|
+
return {
|
|
7
|
+
owner: requiredEnv('REPO_OWNER', env),
|
|
8
|
+
repo: requiredEnv('REPO_NAME', env),
|
|
9
|
+
issueNumber: requiredPositiveInt('ISSUE_NUMBER', env),
|
|
10
|
+
installationId: requiredPositiveInt('INSTALLATION_ID', env),
|
|
11
|
+
appId: requiredEnv('APP_ID', env),
|
|
12
|
+
privateKey: requiredEnv('PRIVATE_KEY', env),
|
|
13
|
+
githubToken: requiredEnv('GITHUB_TOKEN', env),
|
|
14
|
+
geminiApiKey: requiredEnv('GEMINI_API_KEY', env),
|
|
15
|
+
openRouterApiKey: requiredEnv('OPENROUTER_API_KEY', env),
|
|
16
|
+
commentId,
|
|
17
|
+
reactionId,
|
|
18
|
+
deliveryId: env.DELIVERY_ID,
|
|
19
|
+
dryRun,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { logger } from '../../core/logging.js';
|
|
2
|
+
const MAX_MEDIA_REFERENCES = 25;
|
|
3
|
+
const MAX_MEDIA_IMAGES_FOR_PROMPT = 8;
|
|
4
|
+
const MAX_MEDIA_IMAGE_BYTES = 2_000_000;
|
|
5
|
+
const MAX_MEDIA_TOTAL_BYTES = 8_000_000;
|
|
6
|
+
const MAX_MEDIA_SOURCE_CONTEXT_CHARS = 12_000;
|
|
7
|
+
const IMAGE_EXTENSION_REGEX = /\.(png|jpe?g|webp|bmp|svg|heic|heif)(\?.*)?$/i;
|
|
8
|
+
const UNSUPPORTED_MEDIA_EXTENSION_REGEX = /\.(gif|mp4|mov|avi|webm|mkv|m4v)(\?.*)?$/i;
|
|
9
|
+
const HTML_IMAGE_REGEX = /<img[^>]*\bsrc=["'](https?:\/\/[^"'\s>]+)["'][^>]*>/gi;
|
|
10
|
+
const MARKDOWN_IMAGE_REGEX = /!\[[^\]]*]\((https?:\/\/[^)\s]+)\)/gi;
|
|
11
|
+
const MARKDOWN_LINK_REGEX = /\[[^\]]*]\((https?:\/\/[^)\s]+)\)/gi;
|
|
12
|
+
const BARE_URL_REGEX = /https?:\/\/[^\s<>()"']+/gi;
|
|
13
|
+
function extractUrlMatches(text) {
|
|
14
|
+
const matches = [];
|
|
15
|
+
for (const match of text.matchAll(HTML_IMAGE_REGEX)) {
|
|
16
|
+
const url = match[1];
|
|
17
|
+
const index = match.index ?? 0;
|
|
18
|
+
if (url) {
|
|
19
|
+
matches.push({ url, index, explicitMedia: true });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const match of text.matchAll(MARKDOWN_IMAGE_REGEX)) {
|
|
23
|
+
const url = match[1];
|
|
24
|
+
const index = match.index ?? 0;
|
|
25
|
+
if (url) {
|
|
26
|
+
matches.push({ url, index, explicitMedia: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (const match of text.matchAll(MARKDOWN_LINK_REGEX)) {
|
|
30
|
+
const url = match[1];
|
|
31
|
+
const index = match.index ?? 0;
|
|
32
|
+
if (url) {
|
|
33
|
+
matches.push({ url, index, explicitMedia: false });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
for (const match of text.matchAll(BARE_URL_REGEX)) {
|
|
37
|
+
const url = match[0];
|
|
38
|
+
const index = match.index ?? 0;
|
|
39
|
+
if (url) {
|
|
40
|
+
matches.push({ url, index, explicitMedia: false });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return matches;
|
|
44
|
+
}
|
|
45
|
+
function normalizeUrl(rawUrl) {
|
|
46
|
+
return rawUrl.replace(/[)\],.;!?"'>]+$/, '');
|
|
47
|
+
}
|
|
48
|
+
function isMediaCandidate(url, explicitMedia) {
|
|
49
|
+
if (UNSUPPORTED_MEDIA_EXTENSION_REGEX.test(url)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
if (IMAGE_EXTENSION_REGEX.test(url)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(url);
|
|
57
|
+
if (parsed.pathname.includes('/user-attachments/')) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
return explicitMedia;
|
|
65
|
+
}
|
|
66
|
+
function snippetAround(text, index, radius = 180) {
|
|
67
|
+
const start = Math.max(0, index - radius);
|
|
68
|
+
const end = Math.min(text.length, index + radius);
|
|
69
|
+
return text.slice(start, end).replace(/\s+/g, ' ').trim();
|
|
70
|
+
}
|
|
71
|
+
export function extractMediaReferences(entries, maxReferences = MAX_MEDIA_REFERENCES) {
|
|
72
|
+
const references = [];
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
const sourceTexts = [entry.body, entry.bodyHtml ?? ''];
|
|
76
|
+
for (const sourceText of sourceTexts) {
|
|
77
|
+
if (!sourceText) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const matches = extractUrlMatches(sourceText);
|
|
81
|
+
for (const match of matches) {
|
|
82
|
+
const url = normalizeUrl(match.url);
|
|
83
|
+
if (!isMediaCandidate(url, match.explicitMedia)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const dedupeKey = `${entry.source}:${entry.id}:${url}`;
|
|
87
|
+
if (seen.has(dedupeKey)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
seen.add(dedupeKey);
|
|
91
|
+
references.push({
|
|
92
|
+
url,
|
|
93
|
+
source: entry.source,
|
|
94
|
+
sourceId: entry.id,
|
|
95
|
+
sourceLabel: entry.source === 'issue_body'
|
|
96
|
+
? `Issue description (issue #${entry.id})`
|
|
97
|
+
: `Comment #${entry.id}`,
|
|
98
|
+
author: entry.author,
|
|
99
|
+
sourceBody: entry.body,
|
|
100
|
+
contextSnippet: snippetAround(sourceText, match.index),
|
|
101
|
+
});
|
|
102
|
+
if (references.length >= maxReferences) {
|
|
103
|
+
return references;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return references;
|
|
109
|
+
}
|
|
110
|
+
function buildMediaRequestHeaders({ includeRange }) {
|
|
111
|
+
const headers = new Headers({
|
|
112
|
+
Accept: 'image/*;q=0.9,*/*;q=0.8',
|
|
113
|
+
'User-Agent': 'doistbot-media-fetch/1.0',
|
|
114
|
+
});
|
|
115
|
+
if (includeRange) {
|
|
116
|
+
headers.set('Range', 'bytes=0-1023');
|
|
117
|
+
}
|
|
118
|
+
return headers;
|
|
119
|
+
}
|
|
120
|
+
function isImageContentType(contentType) {
|
|
121
|
+
if (!contentType) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const mimeType = contentType.toLowerCase().split(';', 1)[0]?.trim() ?? '';
|
|
125
|
+
return mimeType.startsWith('image/') && mimeType !== 'image/gif';
|
|
126
|
+
}
|
|
127
|
+
export async function inspectMediaReferences(references, fetcherOrOptions = fetch) {
|
|
128
|
+
const { fetcher } = typeof fetcherOrOptions === 'function'
|
|
129
|
+
? { fetcher: fetcherOrOptions }
|
|
130
|
+
: {
|
|
131
|
+
fetcher: fetcherOrOptions.fetcher ?? fetch,
|
|
132
|
+
};
|
|
133
|
+
const inspections = [];
|
|
134
|
+
for (const reference of references) {
|
|
135
|
+
try {
|
|
136
|
+
let response = await fetcher(reference.url, {
|
|
137
|
+
method: 'HEAD',
|
|
138
|
+
redirect: 'follow',
|
|
139
|
+
headers: buildMediaRequestHeaders({
|
|
140
|
+
includeRange: false,
|
|
141
|
+
}),
|
|
142
|
+
signal: AbortSignal.timeout(10_000),
|
|
143
|
+
});
|
|
144
|
+
const headContentType = response.headers.get('content-type')?.toLowerCase();
|
|
145
|
+
const shouldRetryWithGet = !response.ok || !headContentType || headContentType.startsWith('text/html');
|
|
146
|
+
if (shouldRetryWithGet) {
|
|
147
|
+
response = await fetcher(reference.url, {
|
|
148
|
+
method: 'GET',
|
|
149
|
+
redirect: 'follow',
|
|
150
|
+
headers: buildMediaRequestHeaders({
|
|
151
|
+
includeRange: true,
|
|
152
|
+
}),
|
|
153
|
+
signal: AbortSignal.timeout(10_000),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (!response.ok) {
|
|
157
|
+
inspections.push({
|
|
158
|
+
url: reference.url,
|
|
159
|
+
sourceLabel: reference.sourceLabel,
|
|
160
|
+
author: reference.author,
|
|
161
|
+
contextSnippet: reference.contextSnippet,
|
|
162
|
+
status: 'fetch_failed',
|
|
163
|
+
note: `Failed to fetch media (${response.status} ${response.statusText}).`,
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const contentType = response.headers.get('content-type') ?? 'unknown';
|
|
168
|
+
const rawContentLength = response.headers.get('content-length');
|
|
169
|
+
const contentLength = rawContentLength ? Number(rawContentLength) : undefined;
|
|
170
|
+
const isMedia = isImageContentType(contentType);
|
|
171
|
+
if (!isMedia) {
|
|
172
|
+
inspections.push({
|
|
173
|
+
url: reference.url,
|
|
174
|
+
sourceLabel: reference.sourceLabel,
|
|
175
|
+
author: reference.author,
|
|
176
|
+
contextSnippet: reference.contextSnippet,
|
|
177
|
+
status: 'not_media',
|
|
178
|
+
note: `Fetched URL but content type is "${contentType}".`,
|
|
179
|
+
contentType,
|
|
180
|
+
contentLength: Number.isFinite(contentLength) ? contentLength : undefined,
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
inspections.push({
|
|
185
|
+
url: reference.url,
|
|
186
|
+
sourceLabel: reference.sourceLabel,
|
|
187
|
+
author: reference.author,
|
|
188
|
+
contextSnippet: reference.contextSnippet,
|
|
189
|
+
status: 'fetched',
|
|
190
|
+
note: 'Fetched media metadata successfully.',
|
|
191
|
+
contentType,
|
|
192
|
+
contentLength: Number.isFinite(contentLength) ? contentLength : undefined,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
inspections.push({
|
|
197
|
+
url: reference.url,
|
|
198
|
+
sourceLabel: reference.sourceLabel,
|
|
199
|
+
author: reference.author,
|
|
200
|
+
contextSnippet: reference.contextSnippet,
|
|
201
|
+
status: 'fetch_failed',
|
|
202
|
+
note: `Media fetch failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return inspections;
|
|
207
|
+
}
|
|
208
|
+
function truncateForMediaSourceContext(body) {
|
|
209
|
+
if (body.length <= MAX_MEDIA_SOURCE_CONTEXT_CHARS) {
|
|
210
|
+
return body;
|
|
211
|
+
}
|
|
212
|
+
return `${body.slice(0, MAX_MEDIA_SOURCE_CONTEXT_CHARS)}\n\n[...truncated media source context due size limit...]`;
|
|
213
|
+
}
|
|
214
|
+
function mediaReferenceKey(reference) {
|
|
215
|
+
return `${reference.url}::${reference.sourceLabel}`;
|
|
216
|
+
}
|
|
217
|
+
function buildMediaSourceContext(reference) {
|
|
218
|
+
const sourceType = reference.source === 'issue_body'
|
|
219
|
+
? 'Issue description'
|
|
220
|
+
: `Comment ${reference.sourceLabel.replace('Comment #', '#')}`;
|
|
221
|
+
return [
|
|
222
|
+
`Source type: ${sourceType}`,
|
|
223
|
+
`Source author: ${reference.author}`,
|
|
224
|
+
'',
|
|
225
|
+
'Source text:',
|
|
226
|
+
truncateForMediaSourceContext(reference.sourceBody),
|
|
227
|
+
].join('\n');
|
|
228
|
+
}
|
|
229
|
+
export async function buildMediaPromptImages(references, inspections, options = {}) {
|
|
230
|
+
const fetcher = options.fetcher ?? fetch;
|
|
231
|
+
const maxImages = options.maxImages ?? MAX_MEDIA_IMAGES_FOR_PROMPT;
|
|
232
|
+
const inspectionMap = new Map(inspections.map((inspection) => [mediaReferenceKey(inspection), inspection]));
|
|
233
|
+
const images = [];
|
|
234
|
+
let totalBytes = 0;
|
|
235
|
+
for (const reference of references) {
|
|
236
|
+
if (images.length >= maxImages) {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
const inspection = inspectionMap.get(mediaReferenceKey(reference));
|
|
240
|
+
if (inspection?.status !== 'fetched') {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const inspectionContentType = inspection.contentType?.toLowerCase();
|
|
244
|
+
if (!inspectionContentType || !isImageContentType(inspectionContentType)) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const response = await fetcher(reference.url, {
|
|
249
|
+
method: 'GET',
|
|
250
|
+
redirect: 'follow',
|
|
251
|
+
headers: buildMediaRequestHeaders({
|
|
252
|
+
includeRange: false,
|
|
253
|
+
}),
|
|
254
|
+
signal: AbortSignal.timeout(15_000),
|
|
255
|
+
});
|
|
256
|
+
if (!response.ok) {
|
|
257
|
+
logger.warn('Failed to fetch full image bytes for summarize multimodal context', {
|
|
258
|
+
url: reference.url,
|
|
259
|
+
status: response.status,
|
|
260
|
+
statusText: response.statusText,
|
|
261
|
+
});
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const contentType = response.headers.get('content-type') ?? inspection.contentType ?? '';
|
|
265
|
+
if (!isImageContentType(contentType)) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const declaredLength = Number(response.headers.get('content-length') ?? '');
|
|
269
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_MEDIA_IMAGE_BYTES) {
|
|
270
|
+
logger.warn('Skipping image that exceeds per-image size budget', {
|
|
271
|
+
url: reference.url,
|
|
272
|
+
declaredLength,
|
|
273
|
+
limit: MAX_MEDIA_IMAGE_BYTES,
|
|
274
|
+
});
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
278
|
+
if (bytes.length > MAX_MEDIA_IMAGE_BYTES) {
|
|
279
|
+
logger.warn('Skipping image that exceeds per-image size budget after download', {
|
|
280
|
+
url: reference.url,
|
|
281
|
+
bytes: bytes.length,
|
|
282
|
+
limit: MAX_MEDIA_IMAGE_BYTES,
|
|
283
|
+
});
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (totalBytes + bytes.length > MAX_MEDIA_TOTAL_BYTES) {
|
|
287
|
+
logger.warn('Stopping multimodal image collection due total size budget', {
|
|
288
|
+
nextUrl: reference.url,
|
|
289
|
+
currentTotalBytes: totalBytes,
|
|
290
|
+
nextBytes: bytes.length,
|
|
291
|
+
limit: MAX_MEDIA_TOTAL_BYTES,
|
|
292
|
+
});
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
totalBytes += bytes.length;
|
|
296
|
+
images.push({
|
|
297
|
+
url: reference.url,
|
|
298
|
+
sourceLabel: reference.sourceLabel,
|
|
299
|
+
author: reference.author,
|
|
300
|
+
sourceContext: buildMediaSourceContext(reference),
|
|
301
|
+
contentType,
|
|
302
|
+
dataBase64: bytes.toString('base64'),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
logger.warn('Failed to collect image bytes for summarize multimodal context', {
|
|
307
|
+
url: reference.url,
|
|
308
|
+
error: error instanceof Error ? error.message : String(error),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return images;
|
|
313
|
+
}
|
|
314
|
+
export function formatMediaContext(mediaInspections) {
|
|
315
|
+
if (mediaInspections.length === 0) {
|
|
316
|
+
return 'No candidate screenshot/video links were detected in the issue description or comments.';
|
|
317
|
+
}
|
|
318
|
+
return mediaInspections
|
|
319
|
+
.map((inspection, index) => {
|
|
320
|
+
const contentType = inspection.contentType
|
|
321
|
+
? `, content-type: ${inspection.contentType}`
|
|
322
|
+
: '';
|
|
323
|
+
const contentLength = inspection.contentLength != null
|
|
324
|
+
? `, content-length: ${inspection.contentLength}`
|
|
325
|
+
: '';
|
|
326
|
+
return [
|
|
327
|
+
`${index + 1}. URL: ${inspection.url}`,
|
|
328
|
+
` Source: ${inspection.sourceLabel} (author: ${inspection.author})`,
|
|
329
|
+
` Fetch status: ${inspection.status}${contentType}${contentLength}`,
|
|
330
|
+
` Note: ${inspection.note}`,
|
|
331
|
+
` Context snippet: ${inspection.contextSnippet || '(none)'}`,
|
|
332
|
+
].join('\n');
|
|
333
|
+
})
|
|
334
|
+
.join('\n\n');
|
|
335
|
+
}
|