fix(agent-kernel): address Oracle review round 2 findings

- tryParseFinalSubmission: return result when summaryMarkdown is non-empty even with empty findings
- consecutiveToolFailures: append tool result to transcript before checking termination threshold
- fingerprint: add buildLegacyFingerprint and dual-index existingPublished for migration compatibility
- regression tests: empty findings+summary, clip newline boundary, fingerprint format migration
This commit is contained in:
jeffusion
2026-05-26 23:42:13 +08:00
committed by Jeffusion
parent 44d52cddc5
commit 27f4ac6a18
6 changed files with 74 additions and 11 deletions

View File

@@ -209,16 +209,6 @@ export class MainAgentRunner {
consecutiveToolFailures = 0;
}
if (!result.ok && consecutiveToolFailures >= maxConsecutiveToolFailures) {
return this.finish(
sessionId,
'max_consecutive_tool_failures',
turns,
toolCalls,
messages
);
}
this.transcriptRepository.appendToolCall({
sessionId,
messageId: assistantRecord.id,
@@ -244,6 +234,16 @@ export class MainAgentRunner {
result,
},
});
if (!result.ok && consecutiveToolFailures >= maxConsecutiveToolFailures) {
return this.finish(
sessionId,
'max_consecutive_tool_failures',
turns,
toolCalls,
messages
);
}
}
}
}

View File

@@ -98,4 +98,20 @@ describe('applyDeterministicPublishAdapter deduplication', () => {
expect(result.findings).toHaveLength(1);
expect(result.findings[0].severity).toBe('high');
});
it('fingerprint migration: legacy colon-format matches new JSON tuple format', () => {
const category = 'security';
const path = 'src/auth.ts';
const line = 42;
const title = 'SQL injection';
const legacy = createHash('sha256')
.update(`${category}:${path}:${line}:${title}`)
.digest('hex')
.slice(0, 24);
const modern = createHash('sha256')
.update(JSON.stringify([category, path, line, title]))
.digest('hex')
.slice(0, 24);
expect(legacy).not.toBe(modern);
});
});

View File

@@ -34,6 +34,18 @@ function buildFingerprint(category: string, path: string, line: number, title: s
.slice(0, 24);
}
function buildLegacyFingerprint(
category: string,
path: string,
line: number,
title: string
): string {
return createHash('sha256')
.update(`${category}:${path}:${line}:${title}`)
.digest('hex')
.slice(0, 24);
}
function normalizeTitleRoot(title: string): string {
return title
.toLowerCase()
@@ -129,6 +141,15 @@ export async function applyDeterministicPublishAdapter(params: {
const existingPublished = new Map<string, boolean>();
for (const finding of details?.findings ?? []) {
existingPublished.set(finding.fingerprint, finding.published);
const legacy = buildLegacyFingerprint(
finding.category,
finding.path,
finding.line,
finding.title
);
if (legacy !== finding.fingerprint) {
existingPublished.set(legacy, finding.published);
}
}
const findings: Finding[] = normalized.map((finding) => ({

View File

@@ -94,7 +94,7 @@ function tryParseFinalSubmission(text?: string): SubmittedReviewFindings | null
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object') {
const result = normalizeSubmission(parsed);
if (result.findings.length > 0) return result;
if (result.findings.length > 0 || result.summaryMarkdown) return result;
}
} catch {}
return parseSubmissionFromText(text);

View File

@@ -204,4 +204,13 @@ describe('review task tools', () => {
expect(result).toMatchObject({ accepted: false });
expect(state.submittedReview).toEqual({ summaryMarkdown: 'Previous', findings: [] });
});
test('normalizeSubmission preserves summaryMarkdown with empty findings', () => {
const result = normalizeSubmission({
summaryMarkdown: 'No issues found.',
findings: [],
});
expect(result.summaryMarkdown).toBe('No issues found.');
expect(result.findings).toEqual([]);
});
});

View File

@@ -121,4 +121,21 @@ describe('TokenCounter dynamic catalog', () => {
// Should not throw
counter.stopRefresh();
});
test('clip respects newline boundary when possible', () => {
const counter = new TokenCounter();
const lines = ['line1', 'line2', 'line3', 'line4', 'line5'];
const text = lines.join('\n');
const budget = 3;
const clipped = counter.clip(text, budget);
expect(clipped).toContain('... [truncated');
expect(clipped.includes('\n')).toBe(true);
});
test('clip falls back to char boundary when no newline near cutoff', () => {
const counter = new TokenCounter();
const text = 'abcdefghij';
const clipped = counter.clip(text, 2);
expect(clipped).toContain('... [truncated');
});
});